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

这篇具有很好参考价值的文章主要介绍了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 bestpractice

type User struct {}

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

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

package redis

import (
        "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 user

func 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)
                <-sigchan
                cancel()
        }()
  
        svr := &ctxpkg.Server{}
        svr.Run(ctx) // ← long running process
        log.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 process
                        return
                default:
                        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 happen

        SendRequest(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.Path
        logger := 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.Time
	closeAt 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.openAt
	got := 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.closeAt
	got := 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 cases
	cases := map[string]struct {
		input time.Time
		want  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,不要害怕犯错。文章来源地址https://www.toymoban.com/news/detail-785850.html

到了这里,关于go最佳实践:如何舒适地编码的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【Golang】go编程语言适合哪些项目开发?

    前言 在当今数字化时代,软件开发已成为各行各业的核心需求之一。 而选择适合的编程语言对于项目的成功开发至关重要。 本文将重点探讨Go编程语言适合哪些项目开发,以帮助读者在选择合适的编程语言时做出明智的决策。 Go 编程语言适合哪些项目开发? Go是由Google开发

    2024年02月04日
    浏览(80)
  • 【Golang】VsCode下开发Go语言的环境配置(超详细图文详解)

    📓推荐网站(不断完善中):个人博客 📌个人主页:个人主页 👉相关专栏:CSDN专栏、个人专栏 🏝立志赚钱,干活想躺,瞎分享的摸鱼工程师一枚 ​ 话说在前,Go语言的编码方式是 UTF-8 ,理论上你直接使用文本进行编辑也是可以的,当然为了提升我们的开发效率我们还是需

    2024年02月07日
    浏览(85)
  • Flutter 最佳实践和编码准则

    最佳实践是一套既定的准则,可以提高代码质量、可读性和可靠性。它们确保遵循行业标准,鼓励一致性,并促进开发人员之间的合作。通过遵循最佳实践,代码变得更容易理解、修改和调试,从而提高整体软件质量。 原文 https://ducafecat.com/blog/flutter-best-practices-and-coding-gui

    2024年02月15日
    浏览(47)
  • Golang中接口类型详解与最佳实践(二)

    之前的文章《Golang中的interface(接口)详解与最佳实践》详细介绍了接口类型的定义、使用方法和最佳实践。接口类型使得编写可扩展、可维护和可复用的高质量代码变得更加容易。 还是使用之前文章的例子,例如声明了如下一个接口MyInterface: 这个接口定义了两个方法Method

    2024年02月03日
    浏览(40)
  • 探索 Awesome Guidelines:编码规范与最佳实践的宝库

    项目地址:https://gitcode.com/Kristories/awesome-guidelines 在软件开发的世界中,遵循一致和高效的编码标准是至关重要的。它不仅提高了代码质量,还能让团队协作更加顺畅。Awesome Guidelines 是一个精心整理的资源库,收集了各种编程语言、工具和技术的最佳实践和指导原则,帮助开发

    2024年04月11日
    浏览(49)
  • Go之流程控制大全: 细节、示例与最佳实践

    本文深入探讨Go语言中的流程控制语法,包括基本的 if-else 条件分支、 for 循环、 switch-case 多条件分支,以及与特定数据类型相关的流程控制,如 for-range 循环和 type-switch 。文章还详细描述了 goto 、 fallthrough 等跳转语句的使用方法,通过清晰的代码示例为读者提供了直观的指

    2024年02月08日
    浏览(33)
  • Go 项目依赖注入wire工具最佳实践介绍与使用

    目录 一、引入 二、控制反转与依赖注入 三、为什么需要依赖注入工具 3.1 示例 3.2 依赖注入写法与非依赖注入写法 四、wire 工具介绍与安装 4.1 wire 基本介绍 4.2 安装 五、Wire 的基本使用 5.1 前置代码准备 5.2 使用 Wire 工具生成代码 六、Wire 核心技术 5.1 抽象语法树分析 5.2 模板

    2024年04月08日
    浏览(43)
  • Go-Zero微服务快速入门和最佳实践(一)

    并发编程和分布式微服务 是我们Gopher升职加薪的关键。 毕竟Go基础很容易搞定,不管你是否有编程经验,都可以比较快速的入门Go语言进行简单项目的开发。 虽说好上手,但是想和别人拉开差距,提高自己的竞争力, 搞懂分布式微服务和并发编程还是灰常重要的,这也是我

    2024年04月28日
    浏览(42)
  • 深入解析Go非类型安全指针:技术全解与最佳实践

    本文全面深入地探讨了Go非类型安全指针,特别是在Go语言环境下的应用。从基本概念、使用场景,到潜在风险和挑战,文章提供了一系列具体的代码示例和最佳实践。目的是帮助读者在保证代码安全和效率的同时,更加精通非类型安全指针的使用。 关注【TechLeadCloud】,分享

    2024年02月08日
    浏览(43)
  • SHA-512在Go中的实战应用: 性能优化和安全最佳实践

    在当今数字化的世界中,数据安全已成为软件开发的核心议题之一。特别是在数据传输和存储过程中,保护数据不被未经授权的访问和篡改是至关重要的。为了达到这一目的,开发者们经常依赖于强大的哈希算法来加强数据的安全性。SHA-512,作为一种被广泛认可和使用的安全

    2024年02月20日
    浏览(45)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包