Golang编码规范之编程模式01

举报
kaliarch 发表于 2022/03/06 11:50:08 2022/03/06
【摘要】 一 表驱动测试当测试逻辑是重复的时候,通过 subtests 使用 table 驱动的方式编写 case 代码看上去会更简洁。Bad// func TestSplitHostPort(t *testing.T)host, port, err := net.SplitHostPort("192.0.2.0:8000")require.NoError(t, err)assert.Equal(t...

一 表驱动测试

当测试逻辑是重复的时候,通过 subtests 使用 table 驱动的方式编写 case 代码看上去会更简洁。

  • Bad
// func TestSplitHostPort(t *testing.T)

host, port, err := net.SplitHostPort("192.0.2.0:8000")
require.NoError(t, err)
assert.Equal(t, "192.0.2.0", host)
assert.Equal(t, "8000", port)

host, port, err = net.SplitHostPort("192.0.2.0:http")
require.NoError(t, err)
assert.Equal(t, "192.0.2.0", host)
assert.Equal(t, "http", port)

host, port, err = net.SplitHostPort(":8000")
require.NoError(t, err)
assert.Equal(t, "", host)
assert.Equal(t, "8000", port)

host, port, err = net.SplitHostPort("1:8")
require.NoError(t, err)
assert.Equal(t, "1", host)
assert.Equal(t, "8", port)
  • Good
// func TestSplitHostPort(t *testing.T)

tests := []struct{
  give     string
  wantHost string
  wantPort string
}{
  {
    give:     "192.0.2.0:8000",
    wantHost: "192.0.2.0",
    wantPort: "8000",
  },
  {
    give:     "192.0.2.0:http",
    wantHost: "192.0.2.0",
    wantPort: "http",
  },
  {
    give:     ":8000",
    wantHost: "",
    wantPort: "8000",
  },
  {
    give:     "1:8",
    wantHost: "1",
    wantPort: "8",
  },
}

for _, tt := range tests {
  t.Run(tt.give, func(t *testing.T) {
    host, port, err := net.SplitHostPort(tt.give)
    require.NoError(t, err)
    assert.Equal(t, tt.wantHost, host)
    assert.Equal(t, tt.wantPort, port)
  })
}

很明显,使用 test table 的方式在代码逻辑扩展的时候,比如新增 test case,都会显得更加的清晰。

我们遵循这样的约定:将结构体切片称为tests。 每个测试用例称为tt。此外,我们鼓励使用givewant前缀说明每个测试用例的输入和输出值。

tests := []struct{
  give     string
  wantHost string
  wantPort string
}{
  // ...
}

for _, tt := range tests {
  // ...
}

二 功能选项

功能选项是一种模式,您可以在其中声明一个不透明 Option 类型,该类型在某些内部结构中记录信息。您接受这些选项的可变编号,并根据内部结构上的选项记录的全部信息采取行动。

将此模式用于您需要扩展的构造函数和其他公共 API 中的可选参数,尤其是在这些功能上已经具有三个或更多参数的情况下。

  • Bad
// package db

func Open(
  addr string,
  cache bool,
  logger *zap.Logger
) (*Connection, error) {
  // ...
}

// 必须始终提供缓存和记录器参数,即使用户希望使用默认值。
db.Open(addr, db.DefaultCache, zap.NewNop())
db.Open(addr, db.DefaultCache, log)
db.Open(addr, false /* cache */, zap.NewNop())
db.Open(addr, false /* cache */, log)
  • Good
// package db

type Option interface {
  // ...
}

func WithCache(c bool) Option {
  // ...
}

func WithLogger(log *zap.Logger) Option {
  // ...
}

// Open creates a connection.
func Open(
  addr string,
  opts ...Option,
) (*Connection, error) {
  // ...
}

// 只有在需要时才提供选项。
db.Open(addr)
db.Open(addr, db.WithLogger(log))
db.Open(addr, db.WithCache(false))
db.Open(
  addr,
  db.WithCache(false),
  db.WithLogger(log),
)

我们建议实现此模式的方法是使用一个 Option 接口,该接口保存一个未导出的方法,在一个未导出的 options 结构上记录选项。

type options struct {
  cache  bool
  logger *zap.Logger
}

type Option interface {
  apply(*options)
}

type cacheOption bool

func (c cacheOption) apply(opts *options) {
  opts.cache = bool(c)
}

func WithCache(c bool) Option {
  return cacheOption(c)
}

type loggerOption struct {
  Log *zap.Logger
}

func (l loggerOption) apply(opts *options) {
  opts.logger = l.Log
}

func WithLogger(log *zap.Logger) Option {
  return loggerOption{Log: log}
}

// Open creates a connection.
func Open(
  addr string,
  opts ...Option,
) (*Connection, error) {
  options := options{
    cache:  defaultCache,
    logger: zap.NewNop(),
  }

  for _, o := range opts {
    o.apply(&options)
  }

  // ...
}

注意:还有一种使用闭包实现这个模式的方法,但是我们相信上面的模式为作者提供了更多的灵活性,并且更容易对用户进行调试和测试。特别是,在不可能进行比较的情况下它允许在测试和模拟中对选项进行比较。此外,它还允许选项实现其他接口,包括 fmt.Stringer,允许用户读取选项的字符串表示形式。

还可以参考下面资料:

三 Linting

比任何 “blessed” linter 集更重要的是,lint 在一个代码库中始终保持一致。

我们建议至少使用以下 linters,因为我认为它们有助于发现最常见的问题,并在不需要规定的情况下为代码质量建立一个高标准:

Lint Runners

我们推荐 golangci-lint 作为 go-to lint 的运行程序,这主要是因为它在较大的代码库中的性能以及能够同时配置和使用许多规范。这个 repo 有一个示例配置文件 .golangci.yml 和推荐的 linter 设置。

golangci-lint 有 various-linters 可供使用。建议将上述 linters 作为基本 set,我们鼓励团队添加对他们的项目有意义的任何附加 linters。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。