关于如何构建 Go 代码的思考

简介: 良好的应用结构可以改善开发者的体验。它可以帮助你隔离他们正在工作的内容,而不需要把整个代码库记在脑子里。一个结构良好的应用程序可以通过解耦组件并使其更容易编写有用的测试来帮助防止错误。

这很复杂,但不必如此

应用程序结构复杂。

良好的应用结构可以改善开发者的体验。它可以帮助你隔离他们正在工作的内容,而不需要把整个代码库记在脑子里。一个结构良好的应用程序可以通过解耦组件并使其更容易编写有用的测试来帮助防止错误。

一个结构不良的应用程序可能会起到相反的作用;它可能会使测试更加困难,它可能会使找到相关的代码成为挑战,它可能会引入不必要的复杂性和冗长的语言,使你的速度变慢而没有任何实际好处。

最后一点很重要 - 使用比需求复杂得多的结构实际上对项目的伤害大于帮助。

我在这里写的东西可能对任何人都不是新闻。程序员很早就被告知组织他们的代码的重要性。无论是命名变量和函数,还是命名和组织文件,这几乎是每一个编程课程中早期涉及的主题。

所有这些都引出了一个问题——为什么要弄清楚如何构建 Go 代码这么难?

按上下文组织

在过去的 Go Time 问答集中,我们被问到有关构建 Go 应用程序的问题,Peter Bourgon 回答如下:

很多语言都有这样的惯例(我猜),对于同一类型的项目,所有的项目结构都大致相同......比如,如果你用 Ruby 做一个 web 服务,你会有这样的布局,包会以你使用的架构模式命名。例如,MVC;控制器等等。但是在 Go 中,这不是我们真正要做的。我们有包和项目结构,基本上反映了我们正在实现的东西的领域。不是我们使用的模式,不是脚手架,而是我们正在工作的项目领域中的特定类型和实体。

因此,根据定义,它总是在习惯上因项目而异。在一个地方有意义的东西在另一个地方就没有意义了。不是说这是做事的唯一方法,但这是我们倾向于做的事情......因此,是的,没有答案,那种关于语言中成语的真相让很多人感到非常困惑,结果可能是错误的选择......我不知道,但我想这是主要的问题。

总的来说,大多数成功的 Go 应用程序的结构并不是可以从一个项目复制/粘贴到另一个项目。也就是说,我们不能采用通用文件夹结构并把其复制到一个新的应用程序中,并期望它能正常工作,因为新的应用程序很可能有一套独特的上下文来工作。

其寻找要复制的模板,不如开始考虑应用程序的上下文。为了帮助您理解我的意思,让我们尝试了解如何构建用于托管我的 Go Courses 的 Web 应用程序。


背景信息:Go courses 应用程序是一个网站,学生在这里注册课程并查看课程中的个别课程。大多数课程都有一个视频部分,链接到课程中使用的代码,以及其他相关信息。如果你曾经使用过任何视频课程网站,你应该对它的外观有一个大致的了解,但如果你想进一步挖掘,你可以免费注册 Gophercises。

在这一点上,我对应用程序的需求相当熟悉,但我要试着引导你了解我最初开始创建应用程序时的思考过程,因为这是你也要经常开始的状态。

让我们开始,我想考虑两个主要界面:

  1. The student context. 学生界面
  2. The admin/teacher context. 管理员老师界面

学生界面是大多数人所熟悉的。在这种情况下,用户登录帐户,查看包含他们有权访问的课程的仪表板,然后可以向下导航到课程中的各个课程。

管理员的界面有点不同,大多数人不会看到。作为一个管理员,我们不太担心消费课程,而更关心如何管理它们。我们需要能够在课程中添加新的课程,更新现有课程的视频,以及更多。除了能够管理课程之外,管理员还需要管理用户、购买和退款。

为了创建这种分离,我的 repo 将从两个包开始:

admin/
  ... (some go files here)
student/
  ... (some go files here)

通过分离这两个包,我能够在每个上下文中以不同的方式定义实体。例如,

从学生的角度来看,课程 Lesson 主要由指向资源的 URL 组成,并且它具有用户特定的信息,如 CompletedAt 字段,该字段用来显示该特定用户何时/是否完成了这门课程。

package student
type Lesson struct {
  Name         string // Name of the lesson, eg: "How to run a test"
  Video        string // URL to the video for this lesson. Empty if the user
                      // doesn't have access to this.
  SourceCode   string // URL to the source code for this lesson.
  CompletedAt  *time.Time // A boolean representing whether or not the lesson
                          // was completed by this user.
  // + more
}

同时,管理员界面的课程类型就没有 CompletedAt 字段,因为这在此上下文中没有意义。该信息仅与查看课程的登录用户的上下文相关,而不是作为管理课程内容的管理员。

相反,管理员课程类型将提供对 Requirement 等字段的访问,这些字段将被用来确定用户是否可以访问内容。其他字段看起来也会有些不同; Video 字段可能不是视频的 URL,而是关于视频的托管地点的信息,因为这是管理员更新内容的方式。

package admin
// Using inline structs for brevity in this example
type Lesson struct {
  Name string
  // A video's URL can be constructed dynamically (and in some cases with time
  // limited access tokens) using this information.
  Video struct {
    Provider string // Youtube, Vimeo, etc
    ExternalID string
  }
  // Information needed to determine the URL of a repo/branch
  SourceCode struct {
    Provider string // Github, Gitlab, etc
    Repo     string // eg "gophercises/quiz"
    Branch   string // eg "solution-p1"
  }
  // Used to determine if a user has access to this lesson.
  // Usually a string like "twg-base", then when a user purchases
  // a course license they will have these permission strings linked to
  // their account. Prob not the most efficient way to do things, but works
  // well enough for now and makes it really easy to make packages down the
  // road that provide access to multiple courses.
  Requirement string
}

我选择走这条路是因为我相信这两种情况的变化足以证明分离是合理的,但我也怀疑两者都不会发展到足以证明任何进一步组织的合理性。

我可以以不同的方式组织这段代码吗?绝对地!

我可能改变结构的一种方式是将其进一步分开。例如,一些进入 admin 包的代码与管理用户有关,而另一些代码与管理课程有关。把它分成两个背景是很容易的。另外,我可以把所有与认证有关的代码--注册、修改密码等--拉出来,放在一个 auth 包里。

与其想太多,我发现选择看起来相当合适的东西并根据需要进行调整更有用。

包作为层

另一种分解应用程序的方法是依赖。 Ben Johnson 在 gobeyond.dev 上对此进行了很好的讨论,特别是在文章包为层,而不是组中。这个概念与 Kat Zien 在她的 GopherCon 演讲“你如何构建你的 Go 应用程序”中提到的六边形架构非常相似。

在高层次上,我们的想法是我们有一个核心领域,我们可以在其中定义我们的资源以及我们用来与之交互的服务。

package app
type Lesson struct {
  ID string
  Name string
  // ...
}
type LessonStore interface {
  Create(*Lesson) error
  QueryByPermissions(...Permission) ([]Lesson, error)
  // ...
}

使用 Lesson 这样的类型和 LessonStore 这样的接口,我们可以编写一个完整的应用程序。如果没有 LessonStore 的实现,我们就无法运行我们的程序,但我们可以编写所有的核心逻辑,而不必担心它是如何实现的。

当我们准备好实现像 LessonStore 这样的接口时,我们会给我们的应用程序添加一个新的层。在这种情况下,它可能是以 sql 包的形式出现的。

package sql
type LessonStore struct {
  db *sql.DB
}
func Create(l *Lesson) error {
  // ...
}
func QueryByPermissions(perms ...Permission) ([]Lesson, error) {
  // ...
}


按层打包的方法似乎与我在 Go courses 中选择的方法大相径庭,但实际上混合匹配这些策略要比最初看起来容易得多。例如,如果我们把管理员和学生各自当作一个定义了资源和服务的域,我们就可以使用按层打包的方法来实现这些服务。下面是一个使用 admin 包域和 sql 包的例子,sql 包有一个 admin.LessonStore 的实现。

package admin
type Lesson struct {
  // ... same as before
}
type LessonStore interface {
  Create(*Lesson) error
  // ...
}
package sql
import "github.com/joncalhoun/my-app/admin"
type AdminLessonStore struct { ... }
func (ls *AdminLessonStore) Create(lesson *admin.Lesson) error { ... }

这是应用程序的正确选择吗?我不知道。

使用这样的接口确实可以更容易地测试较小的代码段,但这只有在它提供真正的好处时才重要。否则,我们最终会编写接口、解耦代码并创建新的包,而没有真正的好处。基本上,我们正在为自己创造忙碌的工作。

唯一错误的决定就是没有决定

除了这些结构之外,还有无数种结构(或没有结构)代码的方法,根据不同的环境,这些方法也是有意义的。我曾在一些项目中尝试过使用扁平结构--一个单一的包,我仍然对这种结构的效果感到震惊。当我刚开始写 Go 代码时,我几乎只使用 MVC 。这不仅比整个社区可能导致你相信的要好,而且它让我摆脱了因不知道如何构造我的应用程序而导致的决策瘫痪,因此,甚至不知道从哪里开始。

在 Q&A Go Time 一集中,我们被问到如何构建 Go 代码,Mat Ryer 表达了没有一套固定的代码结构方式的好处:

我认为虽然说没有真正的方法可以做到这一点可能会很自由,这也意味着你不能真的做错。它适合您的情况。

现在我有很多使用 Go 的经验,我完全同意 Mat。能够决定适合每个应用程序的结构是一种解放。我喜欢没有固定的做事方式,也没有真正错误的方式。尽管现在有这种感觉,但我还记得在我经验不足的时候,因为没有具体的例子而感到非常沮丧。

事实是,如果没有一些经验,决定哪种结构适合您的情况几乎是不可能的,但感觉就像我们在获得任何经验之前被迫做出决定一样。这是一个 catch-22,甚至在我们开始之前就阻止了我们。

catch-22,象征人们处在一种荒谬的两难之中。翻译家黄文范把这种状况翻译为“坑人二十二”。如:一个人因为没有工作经验而不能得到工作,但是他又因为没有工作而得不到工作经验。

我没有放弃,而是选择了使用我所知道的东西--MVC。这使我能够编写代码,获得一些工作,并从这些错误中学习。随着时间的推移,我开始理解其他的代码结构方式,我的应用程序与 MVC 的相似度越来越低,但这是一个非常渐进的过程。我怀疑,如果我强迫自己立即弄好应用程序的结构,我根本就不会成功。我最多只能在经历了大量的挫折之后获得成功。

绝对正确的是,MVC 永远不会像为项目量身定做的应用结构那样提供清晰的信息。同样正确的是,对于一个几乎没有围棋代码结构经验的人来说,发现项目的理想应用结构并不是一个现实的目标。它需要实践、实验和重构来获得正确的结果。MVC 是简单而平易近人的。当我们没有足够的经验或背景来想出更好的东西时,它是一个合理的起点。

总的来说

正如我在本文开头所说,良好的应用程序结构旨在改善开发人员体验。它旨在帮助您以对您有意义的方式组织代码。这并不意味着让新人瘫痪并不确定如何继续。

如果您发现自己卡住了并且不确定如何继续,请问自己什么更有效率 - 保持僵住,还是选择任何应用程序结构并尝试一下?

前者什么也做不了。对于后者,即使你做错了,你也可以从中吸取教训,下次做得更好。这听起来比从不开始要好得多。

相关文章
|
5月前
|
监控 安全 Go
使用Go语言构建网络IP层安全防护
在Go语言中构建网络IP层安全防护是一项需求明确的任务,考虑到高性能、并发和跨平台的优势,Go是构建此类安全系统的合适选择。通过紧密遵循上述步骤并结合最佳实践,可以构建一个强大的网络防护系统,以保障数字环境的安全完整。
134 12
|
8月前
|
监控 Java Go
无感改造,完美监控:Docker 多阶段构建 Go 应用无侵入观测
本文将介绍一种基于 Docker 多阶段构建的无侵入 Golang 应用观测方法,通过此方法用户无需对 Golang 应用源代码或者编译指令做任何改造,即可零成本为 Golang 应用注入可观测能力。
427 85
|
5月前
|
Java Shell Maven
【Azure Container App】构建Java应用镜像时候遇无法编译错误:ERROR [build 10/10] RUN ./mvnw.cmd dependency:go-offline -B -Dproduction package
在部署Java应用到Azure Container App时,构建镜像过程中出现错误:“./mvnw.cmd: No such file or directory”。尽管项目根目录包含mvnw和mvnw.cmd文件,但依然报错。问题出现在Dockerfile构建阶段执行`./mvnw dependency:go-offline`命令时,系统提示找不到可执行文件。经过排查,确认是mvnw文件内容异常所致。最终通过重新生成mvnw文件解决该问题,镜像成功构建。
179 1
|
6月前
|
NoSQL Go API
MCP 官方开源 Registry 注册服务:基于 Go 和 MongoDB 构建
作为 `registry` 项目的贡献者,我很高兴能参与这个社区驱动的开源项目,也期待它不断发展壮大。本文将对 `registry` 服务进行介绍,为项目的推广尽一份绵薄之力。
231 1
MCP 官方开源 Registry 注册服务:基于 Go 和 MongoDB 构建
|
5月前
|
存储 中间件 网络安全
在Go中构建应用级IP防火墙机制
使用Go构建应用级别的IP防火墙机制不仅能够为你的应用程序增加一层额外的安全性,还能够通过自定义中间件的方式让你有更多控制力,来决定哪些客户端可以或不可以访问你的服务。
155 8
|
6月前
|
JSON 编解码 API
Go语言网络编程:使用 net/http 构建 RESTful API
本章介绍如何使用 Go 语言的 `net/http` 标准库构建 RESTful API。内容涵盖 RESTful API 的基本概念及规范,包括 GET、POST、PUT 和 DELETE 方法的实现。通过定义用户数据结构和模拟数据库,逐步实现获取用户列表、创建用户、更新用户、删除用户的 HTTP 路由处理函数。同时提供辅助函数用于路径参数解析,并展示如何设置路由器启动服务。最后通过 curl 或 Postman 测试接口功能。章节总结了路由分发、JSON 编解码、方法区分、并发安全管理和路径参数解析等关键点,为更复杂需求推荐第三方框架如 Gin、Echo 和 Chi。
|
6月前
|
开发框架 JSON 中间件
Go语言Web开发框架实践:使用 Gin 快速构建 Web 服务
Gin 是一个高效、轻量级的 Go 语言 Web 框架,支持中间件机制,非常适合开发 RESTful API。本文从安装到进阶技巧全面解析 Gin 的使用:快速入门示例(Hello Gin)、定义 RESTful 用户服务(增删改查接口实现),以及推荐实践如参数校验、中间件和路由分组等。通过对比标准库 `net/http`,Gin 提供更简洁灵活的开发体验。此外,还推荐了 GORM、Viper、Zap 等配合使用的工具库,助力高效开发。
|
Go 开发工具
百炼-千问模型通过openai接口构建assistant 等 go语言
由于阿里百炼平台通义千问大模型没有完善的go语言兼容openapi示例,并且官方答复assistant是不兼容openapi sdk的。 实际使用中发现是能够支持的,所以自己写了一个demo test示例,给大家做一个参考。
|
8月前
|
人工智能 搜索推荐 程序员
用 Go 语言轻松构建 MCP 客户端与服务器
本文介绍了如何使用 mcp-go 构建一个完整的 MCP 应用,包括服务端和客户端两部分。 - 服务端支持注册工具(Tool)、资源(Resource)和提示词(Prompt),并可通过 stdio 或 sse 模式对外提供服务; - 客户端通过 stdio 连接服务器,支持初始化、列出服务内容、调用远程工具等操作。
1880 4
|
缓存 监控 前端开发
在 Go 语言中实现 WebSocket 实时通信的应用,包括 WebSocket 的简介、Go 语言的优势、基本实现步骤、应用案例、注意事项及性能优化策略,旨在帮助开发者构建高效稳定的实时通信系统
本文深入探讨了在 Go 语言中实现 WebSocket 实时通信的应用,包括 WebSocket 的简介、Go 语言的优势、基本实现步骤、应用案例、注意事项及性能优化策略,旨在帮助开发者构建高效稳定的实时通信系统。
697 1