go最佳实践:如何舒适地编码

什么是 "最佳 "做法?

有很多做法:你可以自己想出来,在互联网上找到,或者从其他语言中拿来,但由于其主观性,并不总是容易说哪一个比另一个好。”最佳”的含义因人而异,也取决于其背景,例如网络应用的最佳实践可能与中间件的最佳实践不一样。
为了写这篇文章,我带着一个问题看了go的实践,那就是 “它在多大程度上让我对写Go感到舒服?”,当我说"语言的最佳实践是什么?"时,那是在我刚接触这门语言,还没有完全适应写这门语言的时候。
当然,还有更多的做法,我在这里不做介绍,但如果你在写go时知道这些做法,就会非常有用,但这三个做法对我在go中的信心影响最大。
这就是我选择"最佳"做法的原因。现在是该上手的时候了。

实践1:package布局

当我开始学习go时,最令人惊讶的事情之一是,go没有像Laravel对PHP,Express对Node那样的网络框架。这意味着在编写网络应用时,如何组织你的代码和包,完全取决于你。虽然在如何组织代码方面拥有自由是一件好事,但如果没有指导原则,很容易迷失方向。
另外,这也是最难达成一致的话题之一;"最佳 "的含义很容易改变,这取决于程序处理的业务逻辑或代码库的大小/成熟度。即使是同一个代码库,当前的软件包组织在6个月后也可能不是最好的。
虽然没有单一的做法可以统治一切,但为了补救这种情况,我将介绍一些准则,希望它们能使决策过程更容易。

准则1:从平面布局开始

除非你知道代码库会很大,并且需要某种预先的包布局,否则最好从平面布局开始,简单地将所有的go文件放在根文件夹中。
这是一个来自github.com/patrickmn/g…软件包的文件结构。

❯ tree
.
├── CONTRIBUTORS
├── LICENSE
├── README.md
├── cache.go
├── cache_test.go
├── sharded.go
└── sharded_test.go

它只有一个领域的关注:对数据缓存,对于像这样的包,甚至不需要包的布局。扁平结构在这种情况下最适合。
但随着代码库的增长,根文件夹会变得很忙,你会开始觉得扁平结构不再是最好的了。是时候把一些文件移到它们自己的包里了。

准则2:创建子包

据我所知,主要有三种模式:直接在根部,在pkg文件夹下,以及在internal文件夹下。

在根部

在根目录下创建一个带有软件包名称的文件夹,并将所有相关文件移到该文件夹下。这样做的好处是:

  • 没有深层次/嵌套的目录
  • 导入路径不杂乱

缺点是根文件夹会变得有点乱,特别是当有其他文件夹如scripts、bin和docs时。

在pkg包下

创建一个名为pkg的目录,把子包放在它下面。好的方面是:

  • 这个名字清楚地表明这个目录包含了子包
  • 你可以保持顶层的清洁

而不好的方面是你需要在导入路径中有pkg,这并不意味着什么,因为很明显你在导入包。
然而,这种模式有一个更大的问题,也是前一种模式的问题:有可能从版本库外部访问子包。
这对私人仓库来说是可以接受的,因为如果发生这种情况,在审查过程中会被注意到,但重要的是要注意什么是公开的,特别是在开放源码的背景下,向后兼容性很重要。一旦你把它公开,你就不能轻易改变它。
有第三个选择来处理这种情况。

在internal包下

如果/internal在导入路径中,go处理包的方式有点不同。如果软件包被放在/internal文件夹下,只有共享/internal之前的路径的软件包才能访问里面的软件包。
例如,如果软件包路径是/a/b/c/internal/d/e/f,只有/a/b/c目录下的软件包可以访问/internal目录下的软件包。这意味着如果你把internal放在根目录下,只有该仓库内的包可以使用子包,而其他仓库不能访问。如果你想拥有子包,同时保持它们的API在内部,这很有用。

准则3:将main移至cmd目录下

把主包放在cmd/<命令名称>目录下也是一种常见的做法。
假设我们有一个用go编写的管理个人笔记的API服务器,用这种模式看起来会是这样。

$ tree
.
├── cmd
│    └── personal-note-api
│        └── main.go
...
├── Makefile
├── go.mod
└── go.sum

要考虑使用这种模式的情况是:

  • 你可能想在一个资源库中拥有多个二进制文件。你可以在cmd下创建任意多的文件夹,只要你想。
  • 有时需要将主包移到其他地方,以避免循环依赖。

准则4:按其责任组织包装

我们已经研究了何时以及如何制作子包,但还有一个大问题:它们应该如何分组?我认为这是最棘手的部分,需要一些时间来适应,主要是因为它在很大程度上受应用程序的领域关注和功能影响。深入了解代码的作用是做出决定的必要条件。
对此,最常见的建议是按照责任来组织。
对于那些熟悉MVC框架的人来说,拥有"model"、“controller”、“service"等包可能感觉很自然。建议不要在go中使用它们。
相反,我们建议使用更多的责任/领域导向的包名,如"用户"或"事务”。

准则5:按依赖关系对子包进行分组

根据它们的依赖关系来命名包,例如"redis"、“kafka"或"pubsub”,在某些情况下提供了明确的抽象性。
想象一下,你有一个这样的接口:

package bestpracticetype User struct {}type UserService interface {User(context.Context, string) (*User, error)
}

而你在redis子包里有一个服务,它是这样实现的:

package redisimport ("github.com/thirdfort/go-bestpractice""github.com/thirdfort/go-redis"
)type UserService struct {...
}func (s *UserService) User(ctx context.Context, id string) (*bestpractice.User, error) {...err := redis.RetrieveHash(ctx, k)...
}

如果消费者(大概是主函数)只依赖于接口,它可以很容易地被替代的实现所取代,如postgres或inmemory。

附加提示1:给包起一个简短的名字

关于命名包的几个要点。

  • 短而有代表性的名称
  • 使用一个词
  • 使用缩略语,但不要让它变得神秘莫测

如果你想使用多个词(如billing_account)怎么办?我能想到的选项是:

  • 为每个词设置一个嵌套包:billing/account
  • 如果没有混淆,就简单地命名为帐户
  • 使用缩略语:billacc

补充提示2:避免重复

这是关于如何命名包内的内容(结构/界面/函数)。go的建议是,在消费包的时候尽量避免重复。例如,如果我们有一个包,内容是这样的:

package userfunc GetUser(ctx context.Context, id string) (*User, error) {...
}

这个包的消费者要这样调用这个函数:user.GetUser(ctx, u.ID)
在函数调用中出现了两次user这个词。即使我们把user这个词从函数中去掉:user.Get,仍然可以看出它返回了一个用户,因为从包的名称中可以看出。go更倾向于简单的名字。
我希望这些准则在决定包的布局时能有所帮助。
让我们来看看关于上下文的第二个实践。

实践2:熟悉context.Context

在95%的情况下,你唯一需要做的就是将调用者提供的上下文传递给需要上下文作为参数的子程序调用。

func (u *User) Store(ctx context.Context) error {...if err := u.Hash.Store(ctx, k, u); err != nil {return err}...
}

尽管如此,由于context在go程序中随处可见,因此了解何时需要它,以及如何使用它是非常重要的。

context的三种用途

首先,也是最重要的一点是,要意识到上下文可以有三种不同的用途:

  • 发送取消信号
  • 设置超时
  • 存储/检索请求的相关值

发送取消信号

context.Context提供了一种机制,可以发送一个信号,告诉收到context的进程停止。
例如,优雅关机
当一个服务器收到关闭信号时,它需要"优雅地"停止;如果它正在处理一个请求,它需要在关闭之前为其提供服务。context包提供了context.WithCancel API,它返回一个配置了cancel的新上下文和一个取消它的函数。如果你调用cancel函数,信号会被发送到接收该上下文的进程中。
在下面的例子中,它调用context.WithCancel后,在启动服务器时将其传递给服务器。当程序收到OS信号时,会调用cancel:

func main() {ctx, cancel := context.WithCancel(context.Background())defer cancel()go func() {sigchan := make(chan os.Signal, 1)signal.Notify(sigchan, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)<-sigchancancel()}()svr := &ctxpkg.Server{}svr.Run(ctx) // ← long running processlog.Println("graceful stop")
}

让我们看看"伪"服务器的实现;它实际上什么也没做,但为了演示,它有足够的功能:

type Server struct{}func (s *Server) Run(ctx context.Context) {for {select {case <-ctx.Done():log.Println("cancel received, attempting graceful stop...")// clean up processreturndefault:handleRequest()}}
}func handleRequest() {time.Sleep(time.Duration(rand.Intn(10)) * time.Second)
}

它首先进入一个无限的循环。在这个循环中,它检查上下文是否已经在ctx.Done()通道上使用select取消了。如果取消了,它就清理进程并返回。如果没有,它就处理一个请求。一旦请求被处理,它就回到循环中,再次检查上下文。
这里的重点是通过使用context.Context,你可以允许进程在他们准备好的时候返回。

设置超时

第二种用法是为操作设置超时。想象一下,你正在向第三方发送HTTP请求。如果由于某些原因,如网络中断,请求的时间超过预期,你可能想取消请求,以防止整个过程挂起。通过context.WithTimeout,你可以为这些情况设置超时。

func main() {ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)defer cancel() // ← cancel should be called even if timeout didn't happenSendRequest(ctx) // ← subroutine that can get stuck
}

在SendRequest方法中,在不同的goroutine中发送请求后,它同时在ctx.Done()通道和响应通道中等待。当超时发生时,你会从ctx.Done()通道得到一个信号,这样你就可以从该函数中退出,而不用等待响应。

func SendRequest(ctx context.Context) {respCh := make(chan interface{}, 1)go sendRequest(respCh)select {case <-ctx.Done():log.Println("operation timed out!")case <-respCh:log.Println("response received")}
}func sendRequest(ch chan<- interface{}) {time.Sleep(60 * time.Second)ch <- struct{}{}
}

context包也有context.WithDeadline();不同的是,context.WithTimeout需要time.Duration,而context.WithDeadline()需要time.Time。

存储/检索请求的相关值

上下文的最后一种用法是在上下文中存储和检索与请求相关的值。例如,如果服务器收到一个请求,你可能希望在请求过程中产生的所有日志行都有请求信息,如路径和方法。在这种情况下,你可以创建一个日志记录器,设置请求相关的信息,并使用context.WithValue将其存储在上下文中。

var logCtxKey = &struct{}{}func handleRequest(w http.ResponseWriter, r *http.Request) {method, path := r.Method, r.URL.Pathlogger := log.With().Str("method", method).Str("path", path).Logger()ctxWithLogger := context.WithValue(r.Context(), logCtxKey, logger)...accessDatabase(ctxWithLogger)
}

在某个地方,你可以用同样的键把记录器从上下文中取出来。例如,如果你想在数据库访问层留下一个日志,你可以这样做:

func accessDatabase(ctx context.Context) {logger := ctx.Value(logCtxKey).(zerolog.Logger)logger.Debug().Msg("accessing database")
}

这产生了以下包含请求方法和路径的日志行。

{"level":"debug","method":"GET","path":"/v1/todo","time":"2022-11-15T15:44:53Z","message":"accessing database"}

就像我说的,你需要使用这些上下文API的情况并不常见,但了解它的作用真的很重要,这样你就知道在哪种情况下你真的需要注意它。
让我们进入最后一个实践。

实践3:了解 Table Driven Test(表格驱动方法)

表驱动测试是一种组织测试的技术,更多地关注输入数据/模拟/存根和预期输出,而不是断言,这有时可能是重复的。
我选择这种方法的原因不仅是因为这是一种常用的做法,而且这也使我在编写测试时更有乐趣。在编写测试时有一个良好的动机,对于有一个快乐的编码生活是非常重要的,不用说编写可靠的代码。
让我们来看看一个例子。
假设我们有一个餐厅的数据类型,它有一个方法,如果它在某一特定时间开放,则返回真。

type Restaurant struct {openAt  time.TimecloseAt time.Time
}func (r Restaurant) IsOpen(at time.Time) bool {return (at.Equal(r.openAt) || at.After(r.openAt)) &&(at.Equal(r.closeAt) || at.Before(r.closeAt))
}

让我们为这个方法写一些测试。

如果我们在餐厅开门的时候访问了它,我们期望它是开放的。

func TestRestaurantJustOpened(t *testing.T) {r := Restaurant{openAt:  time.Date(2022, time.January, 17, 12, 0, 0, 0, time.UTC),closeAt: time.Date(2022, time.January, 17, 22, 0, 0, 0, time.UTC),}input := r.openAtgot := r.IsOpen(input)assert.True(t, got)
}

到目前为止还不错。让我为边界条件添加更多测试:

func TestRestaurantBeforeOpen(t *testing.T) {r := Restaurant{openAt:  time.Date(2022, time.January, 17, 12, 0, 0, 0, time.UTC),closeAt: time.Date(2022, time.January, 17, 22, 0, 0, 0, time.UTC),}input := r.openAt.Add(-1 * time.Second)got := r.IsOpen(input)assert.False(t, got)
}func TestRestaurantBeforeClose(t *testing.T) {r := Restaurant{openAt:  time.Date(2022, time.January, 17, 12, 0, 0, 0, time.UTC),closeAt: time.Date(2022, time.January, 17, 22, 0, 0, 0, time.UTC),}input := r.closeAtgot := r.IsOpen(input)assert.True(t, got)
}

你可能已经注意到,这些测试之间的差异非常小,我认为这是表驱动测试的一个典型用例。

表驱动测试的介绍

现在让我们看看,如果用表驱动的方式来写,会是什么样子:

func TestRestaurantTableDriven(t *testing.T) {r := Restaurant{openAt:  time.Date(2022, time.January, 17, 12, 0, 0, 0, time.UTC),closeAt: time.Date(2022, time.January, 17, 22, 0, 0, 0, time.UTC),}// test casescases := map[string]struct {input time.Timewant  bool}{"before open": {input: r.openAt.Add(-1 * time.Second),want:  false,},"just opened": {input: r.openAt,want:  true,},"before close": {input: r.closeAt,want:  true,},"just closed": {input: r.closeAt.Add(1 * time.Second),want:  false,},}for name, c := range cases {t.Run(name, func(t *testing.T) {got := r.IsOpen(c.input)assert.Equal(t, c.want, got)})}
}

首先,我声明了测试目标。根据情况,它可以在每个测试案例里面。
接下来,我定义了测试用例。我在这里使用了map,所以我可以使用测试名称作为map键。测试用例结构包含每个情况下的输入和预期输出。
最后,我对测试用例进行了循环,并对每个测试用例运行了子测试。断言与之前的例子相同,但这里我从测试用例结构中获取输入和预期值。
以表格驱动方式编写的测试很紧凑,重复性较低,如果你想添加更多的测试,你只需要添加一个新的测试用例,无需更多的断言。

去尝试吧!

一方面,了解社区中共享的实践很重要。go社区足够大,很容易找到它们。你可以找到博客文章、讲座、YouTube视频等等。另外,说到go,很多实践都来自go的标准库。表驱动测试就是一个很好的例子。go是一种开源语言。阅读标准包代码是个好主意。
另一方面,仅仅知道它们并不能让你感到舒服。到目前为止,学习最佳实践的最好方法是在你现在工作的真实代码库中使用它们,看看它们有多合适,这实际上是我学习go实践的方式。所以,多写go,不要害怕犯错。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/618349.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数据结构学习之顺序栈应用的案例(有效的括号)

实例要求&#xff1a; 给定一个只包括 (&#xff0c;)&#xff0c;{&#xff0c;}&#xff0c;[&#xff0c;] 的字符串 s &#xff0c;判断字符串是否有效&#xff1b; 有效字符串需满足的条件&#xff1a; 1、左括号必须用相同类型的右括号闭合&#xff1b; 2、左括号必须…

CSAPP - 流程化的人工反汇编 string_length, strings_not_equal

文章目录 反汇编的流程string_length 的反汇编&#xff0c;第二次尝试strings_not_equal 反汇编&#xff0c;第二次尝试一些“定式”的整理定式1&#xff1a; cmp 和 je/jne定式2&#xff1a;test A,A 和 je/jne 反汇编的流程 依然是 CSAPP bomblab phase_1 的小白视角的理解。…

c# ref和out参数修饰符

ref&#xff1a;把值类型变成引用类型传递&#xff0c;形参的值改变了实参的值也会改变 public static int findMax(ref int num1, ref int num2){num1 * 2;num2 * 2;return num1 num2;}private static void Main(string[] args){int a1 1; int a2 2;findMax(ref a1, ref a2…

什么是算法的空间复杂度?

一、问题 常常⽤算法的空间复杂度来评价算法的性能&#xff0c;那么什么是算法的空间复杂度呢&#xff1f; 二、解答 算法的空间复杂度是指在算法的执⾏过程中&#xff0c;需要的辅助空间数量。 辅助空间数量指的不是程序指令、常数、指针等所需要的存储空间&#xff0c;也不是…

Pandas实战100例 | 案例 41: 字符串操作

案例 41: 字符串操作 知识点讲解 Pandas 提供了强大的字符串处理功能&#xff0c;这些功能类似于 Python 的标准字符串方法。你可以对 DataFrame 中的字符串数据执行各种操作&#xff0c;如分割、提取、计算长度等。 字符串分割: 使用 str.split() 分割字符串。提取字符串: …

【面试合集】1.说说你对微信小程序的理解?优缺点?

面试官&#xff1a;说说你对微信小程序的理解&#xff1f;优缺点&#xff1f; 一、是什么 2017年&#xff0c;微信正式推出了小程序&#xff0c;允许外部开发者在微信内部运行自己的代码&#xff0c;开展业务 截至目前&#xff0c;小程序已经成为国内前端的一个重要业务&…

NPM进阶知识与用法详解(二)

文章目录 一、NPM高级用法1. NPM模块发布与私有模块管理2. NPM钩子函数3. NPM包管理与优化 二、NPM与现代化前端工具链1. NPM与Yarn、PNPM的比较2. NPM在Webpack、Vite等构建工具中的应用3. NPM与Monorepo架构 三、总结与展望1. 前端包管理工具发展趋势2. 提高NPM使用效率的建议…

从传统到智能:机器视觉检测赋能PCB行业数字化转型!

PCB板在现代电子设备中是一个重要的组成部分&#xff0c;它是用来集成各种电子元器件的信息载体。在电子领域中&#xff0c;PCB板有着广泛的应用&#xff0c;而它的质量直接影响到产品的性能。随着电子科技技术和电子制造业的发展&#xff0c;贴片元器件的体积 变小&#xff0c…

AOSP 编译

AOSP清华镜像站 [2023-11-21 20:44:21] 内存太小导致编译失败&#xff0c;这里通过删除原来的 swap 然后创建更大的 swap 来解决 # 首先关闭并删除现存的 swap sudo swapoff /swapfile sudo rm /swapfile# 创建新 swap sudo dd if/dev/zero of/swapfile bs1G count32 sudo c…

亚马逊怎么防止店铺关联?

亚马逊&#xff08;Amazon&#xff09;为了确保公平竞争和防止不当行为&#xff0c;采取了一些措施来防止店铺关联&#xff0c;即通过不同的方式将多个店铺相关联&#xff0c;以获取不正当的竞争优势。以下是一些亚马逊防止店铺关联的主要措施&#xff1a; 同一经营者规定&…

在Anaconda(conda)(命令行/Linux )中新建环境安装python版本,删除环境等

1.在命令行终端新建conda环境 例如新建一个叫love的环境 在Windows的Anaconda Prompt 或 macOS/Linux的终端输入 conda create -n love python3.102.激活环境 conda activate love注&#xff1a;运行conda activate则直接激活anaconda的base环境 3.退出环境 conda deactiva…

VMware workstation搭建与安装AlmaLinux-9.2虚拟机

VMware workstation搭建与安装AlmaLinux-9.2虚拟机 适用于需要在VMware workstation平台安装AlmaLinux-9.2&#xff08;最小化安装、无图形化界面&#xff09;虚拟机。 1. 安装准备 1.1 安装平台 Windows 11 1.2. 软件信息 软件名称软件版本安装路径VMware-workstation 1…

openssl3.2 - 官方demo学习 - client-arg.c

文章目录 openssl3.2 - 官方demo学习 - client-arg.c笔记client-arg.cEND openssl3.2 - 官方demo学习 - client-arg.c 笔记 client-arg.c /*! \file client-argc. *//*! * \noteadd _CRT_SECURE_NO_WARNINGS to VS2019 option *//** Copyright 2013-2023 The OpenSSL Proj…

Pandas实战100例 | 案例 42: 数据过滤

案例 42: 数据过滤 知识点讲解 数据过滤是数据处理中的一个基本任务。在 Pandas 中&#xff0c;你可以使用布尔索引来过滤符合特定条件的数据行。 数据过滤: 通过结合条件表达式&#xff08;例如 df[A] > 2 和 df[B] < 5&#xff09;&#xff0c;可以创建一个布尔索引…

vue3 源码解析(4)— createApp 源码的实现

前言 本文是 vue3 源码分析系列的第四篇文章&#xff0c;在使用 vue3 时&#xff0c;我们需要使用 createApp 来创建一个应用实例&#xff0c;然后使用 mount 方法将应用挂载到某个DOM节点上。那么在调用 createApp 时&#xff0c;vue 再背后做了些什么事情呢&#xff1f;在这…

类和对象---C++

类和对象目录 类和对象1.封装1.1 封装的意义1.2 struct和class区别1.3 成员属性设置为私有1.3.1 联系---判断圆和点的位置关系 2.对象的初始化和清理2.1 构造函数和析构函数2.2 构造函数的分类及调用2.2.1无参构造函数调用2.2.2有参构造函数调用2.2.2.1括号法2.2.2.2显式法2.2.…

微信小程序快速入门02(含案例)

&#x1f3e1;浩泽学编程&#xff1a;个人主页 &#x1f525; 推荐专栏&#xff1a;《深入浅出SpringBoot》《java项目分享》 《RabbitMQ》《Spring》《SpringMVC》 &#x1f6f8;学无止境&#xff0c;不骄不躁&#xff0c;知行合一 文章目录 前言一、页面导航1.…

互联网资讯精选:科技爱好者周刊 | 开源日报 No.145

ruanyf/weekly Stars: 37.4k License: NOASSERTION 记录每周值得分享的科技内容&#xff0c;提供大量就业信息。欢迎投稿、推荐或自荐文章/软件/资源&#xff0c;并可通过多种方式进行搜索。 提供丰富的科技内容每周更新可以提交工作/实习岗位支持投稿和推荐功能 GyulyVGC/…

bootloader学习笔记及SD卡启动盘制作

Bootloader介绍 在操作系统运行之前运行的一小段代码&#xff0c;用于将软硬件环境初始化到一个合适的状态&#xff0c;为操作系统的加载和运行做准备&#xff08;其本身不是操作系统&#xff09; Bootloader基本功能 1、初始化软硬件环境 2、引导加载linux内核 3、给linux…

一个无敌的 Python 文件系统监控库

在软件开发和系统管理领域&#xff0c;经常需要监控文件和目录的变化&#xff0c;以便在文件被创建、修改或删除时触发相应的操作。Python Watchdog是一个强大的Python库&#xff0c;它提供了简单而灵活的方式来监控文件系统的变化。本文将详细介绍Python Watchdog的用法和功能…