目录

  • 介绍
  • 指南
    • 接口的指针
    • 接收者和接口
    • 零值 Mutexes 是有效的
    • 复制 Slice 和 Map
    • Defer 的使用
    • channel 的大小是 1 或者 None
    • 枚举值从 1 开始
    • Error 类型
    • Error 包装
    • 处理类型断言失败
    • 避免 Panic
    • 使用 go.uber.org/atomic
  • 性能
    • strconv 优于 fmt
    • 避免 string 到 byte 的转换
  • 代码样式
    • 聚合相似的声明
    • 包的分组导入的顺序
    • 包命名
    • 函数命名
    • 别名导入
    • 函数分组和顺序
    • 减少嵌套
    • 不必要的 else
    • 顶层变量的声明
    • 在不可导出的全局变量前面加上 _
    • 结构体的嵌入
    • 使用字段名去初始化结构体
    • 局部变量声明
    • nil 是一个有效的 slice
    • 减少变量的作用域
    • 避免裸参数
    • 使用原生字符串格式来避免转义
    • 初始化结构体
    • 在 Printf 之外格式化字符串
    • Printf-style 函数的命名
  • 设计模式
    • 表格驱动测试
    • 函数参数可选化

介绍

代码风格是代码的一种约定。用风格这个词可能有点不恰当,因为这些约定涉及到的远比源码文件格式工具 gofmt 所能处理的更多。

本指南的目标是通过详细描述 Uber 在编写 Go 代码时的取舍来管理代码的这种复杂性。这些规则的存在是为了保持代码库的可管理性,同时也允许工程师更高效地使用 go 语言特性。

本指南最初由 Prashant Varanasi 和 Simon Newton 为了让同事们更便捷地使用 go 语言而编写。多年来根据其他人的反馈进行了一些修改。

本文记录了 uber 在使用 go 代码中的一些习惯用法。许多都是 go 语言常见的指南,而其他的则延伸到了一些外部资料:

  1. Effective Go
  2. The Go common mistakes guide

所用的代码在运行 golint 和 go vet 之后不会有报错。建议将编辑器设置为:

  • 保存时运行 goimports
  • 运行 golint 和 go vet 来检查错误

你可以在下面的链接找到 Go tools 对一些编辑器的支持:https://github.com/golang/go/wiki/IDEsAndTextEditorPlugins

指南

接口的指针

你几乎不需要指向接口的指针,应该把接口当作值传递,它的底层数据仍然可以当成一个指针。

一个接口是两个字段:

  1. 指向特定类型信息的指针。你可以认为这是 "type."。
  2. 如果存储的数据是指针,则直接存储。如果数据存储的是值,则存储指向此值的指针。

如果你希望接口方法修改底层数据,则必须使用指针。

接收者和接口

具有值接收者的方法可以被指针和值调用。

例如,

type S struct {
data string
} func (s S) Read() string {
return s.data
} func (s *S) Write(str string) {
s.data = str
} sVals := map[int]S{: {"A"}} // 使用值只能调用 Read 方法
sVals[].Read() // 会编译失败
// sVals[0].Write("test") sPtrs := map[int]*S{: {"A"}} // 使用指针可以调用 Read 和 Write 方法
sPtrs[].Read()
sPtrs[].Write("test")

类似的,即使方法是一个值接收者,但接口仍可以被指针类型所满足。

type F interface {
f()
} type S1 struct{} func (s S1) f() {} type S2 struct{} func (s *S2) f() {} s1Val := S1{}
s1Ptr := &S1{}
s2Val := S2{}
s2Ptr := &S2{} var i F
i = s1Val
i = s1Ptr
i = s2Ptr // 以下不能被编译,因为 s2Val 是一个值,并且 f 没有值接收者
// i = s2Val

Effective Go 对 Pointers vs. Values 分析的不错.

零值 Mutexes 是有效的

零值的 sync.Mutex 和 sync.RWMutex 是有效的,所以你几乎不需要指向 mutex 的指针。


Bad Good
mu := new(sync.Mutex)
mu.Lock()
var mu sync.Mutex
mu.Lock()
var mu sync.Mutex

mu.Lock()
defer mu.Unlock()

如果你使用一个指针指向的结构体,mutex 可以作为一个非指针字段,或者,最好是直接嵌入这个结构体。

  

type smap struct {
sync.Mutex data map[string]string
} func newSMap() *smap {
return &smap{
data: make(map[string]string),
}
} func (m *smap) Get(k string) string {
m.Lock()
defer m.Unlock() return m.data[k]
}
type SMap struct {
mu sync.Mutex data map[string]string
} func NewSMap() *SMap {
return &SMap{
data: make(map[string]string),
}
} func (m *SMap) Get(k string) string {
m.mu.Lock()
defer m.mu.Unlock() return m.data[k]
}
为私有类型或需要实现 Mutex 接口的类型嵌入 对于导出的类型,使用私有锁。

复制 Slice 和 Map

slice 和 map 包含指向底层数据的指针,因此复制的时候需要当心。

接收 Slice 和 Map 作为入参

需要留意的是,如果你保存了作为参数接收的 map 或 slice 的引用,可以通过引用修改它。

Bad Good
func (d *Driver) SetTrips(trips []Trip) {
d.trips = trips
} trips := ...
d1.SetTrips(trips) // Did you mean to modify d1.trips?
trips[] = ...
func (d *Driver) SetTrips(trips []Trip) {
d.trips = make([]Trip, len(trips))
copy(d.trips, trips)
} trips := ...
d1.SetTrips(trips) // We can now modify trips[0] without affecting d1.trips.
trips[] = ...

返回 Slice 和 Map

类似的,当心 map 或者 slice 暴露的内部状态是可以被修改的。

Bad Good
type Stats struct {
sync.Mutex counters map[string]int
} // Snapshot 方法返回当前的状态
func (s *Stats) Snapshot() map[string]int {
s.Lock()
defer s.Unlock() return s.counters
} // snapshot 不再被锁保护
snapshot := stats.Snapshot()
type Stats struct {
sync.Mutex counters map[string]int
} func (s *Stats) Snapshot() map[string]int {
s.Lock()
defer s.Unlock() result := make(map[string]int, len(s.counters))
for k, v := range s.counters {
result[k] = v
}
return result
} // 现在 Snapshot 是一个副本
snapshot := stats.Snapshot()

Defer 的使用

使用 defer 去关闭文件句柄和释放锁等类似的这些资源。

Bad Good
p.Lock()
if p.count < {
p.Unlock()
return p.count
} p.count++
newCount := p.count
p.Unlock() return newCount // 多个返回语句导致很容易忘记释放锁
p.Lock()
defer p.Unlock() if p.count < {
return p.count
} p.count++
return p.count // 更可读

defer 的开销非常小,只有在你觉得你的函数执行需要在纳秒级别的情况下才需要考虑避免使用。使用 defer 换取的可读性是值得的。这尤其适用于具有比简单内存访问更复杂的大型方法,这时其他的计算比 defer 更重要。

channel 的大小是 1 或者 None

channel 的大小通常应该是 1 或者是无缓冲的。默认情况下,channel 是无缓冲的且大小为 0。任何其他的大小都必须经过仔细检查。应该考虑如何确定缓冲的大小,哪些因素可以防止 channel 在负载时填满和阻塞写入,以及当这种情况发生时会造成什么样的影响。

Bad Good
// Ought to be enough for anybody!
c := make(chan int, )
// size 为 1
c := make(chan int, ) // 或者
// 非缓冲 channel,size 为 0
c := make(chan int)

枚举值从 1 开始

在 Go 中引入枚举的标准方法是声明一个自定义类型和一个带 iota 的 const 组。由于变量的默认值为 0,因此通常应该以非零值开始枚举。

Bad Good
type Operation int

const (
Add Operation = iota
Subtract
Multiply
) // Add=0, Subtract=1, Multiply=2
type Operation int

const (
Add Operation = iota +
Subtract
Multiply
) // Add=1, Subtract=2, Multiply=3

在某些情况下,使用零值是有意义的,例如零值是想要的默认值。

type LogOutput int

const (
LogToStdout LogOutput = iota
LogToFile
LogToRemote
) // LogToStdout=0, LogToFile=1, LogToRemote=2

Error 类型

声明 error 有多种选项:

返回 error 时,可以考虑以下因素以确定最佳选择:

  • 不需要额外信息的一个简单的 error? 那么 errors.New 就够了
  • 客户端需要检查并处理这个 error?那么应该使用实现了 Error() 方法的自定义类型
  • 是否需要传递下游函数返回的 error?那么请看 section on error wrapping
  • 否则, 可以使用 fmt.Errorf

如果客户端需要检查这个 error,你需要使用 errors.New 和 var 来创建一个简单的 error。

Bad Good
// package foo

func Open() error {
return errors.New("could not open")
} // package bar func use() {
if err := foo.Open(); err != nil {
if err.Error() == "could not open" {
// handle
} else {
panic("unknown error")
}
}
}
// package foo

var ErrCouldNotOpen = errors.New("could not open")

func Open() error {
return ErrCouldNotOpen
} // package bar if err := foo.Open(); err != nil {
if err == foo.ErrCouldNotOpen {
// handle
} else {
panic("unknown error")
}
}

如果你有一个 error 可能需要客户端去检查,并且你想增加更多的信息(例如,它不是一个简单的静态字符串),这时候你需要使用自定义类型。

Bad Good
func open(file string) error {
return fmt.Errorf("file %q not found", file)
} func use() {
if err := open(); err != nil {
if strings.Contains(err.Error(), "not found") {
// handle
} else {
panic("unknown error")
}
}
}
type errNotFound struct {
file string
} func (e errNotFound) Error() string {
return fmt.Sprintf("file %q not found", e.file)
} func open(file string) error {
return errNotFound{file: file}
} func use() {
if err := open(); err != nil {
if _, ok := err.(errNotFound); ok {
// handle
} else {
panic("unknown error")
}
}
}

在直接导出自定义 error 类型的时候需要小心,因为它已经是包的公共 API。最好暴露一个 matcher 函数(译者注:以下示例的 IsNotFoundError 函数)去检查 error。

// package foo

type errNotFound struct {
file string
} func (e errNotFound) Error() string {
return fmt.Sprintf("file %q not found", e.file)
} func IsNotFoundError(err error) bool {
_, ok := err.(errNotFound)
return ok
} func Open(file string) error {
return errNotFound{file: file}
} // package bar if err := foo.Open("foo"); err != nil {
if foo.IsNotFoundError(err) {
// handle
} else {
panic("unknown error")
}
}

Error 包装

如果调用失败,有三个主要选项用于 error 传递:

  • 如果没有额外增加的上下文并且你想维持原始 error 类型,那么返回原始 error
  • 使用 "pkg/errors".Wrap 增加上下文,以至于 error 信息提供更多的上下文,并且 "pkg/errors".Cause 可以用来提取原始 error
  • 如果调用者不需要检查或者处理具体的 error 例子,那么使用 fmt.Errorf

推荐去增加上下文信息取代描述模糊的 error,例如 "connection refused",应该返回例如 "failed to call service foo: connection refused" 这样更有用的 error。

请参考 Don't just check errors, handle them gracefully.

处理类型断言失败

简单的返回值形式的类型断言在断言不正确的类型时将会 panic。因此,需要使用 ", ok" 的常用方式。

Bad Good
t := i.(string)
t, ok := i.(string)
if !ok {
// handle the error gracefully
}

避免 Panic

生产环境跑的代码必须避免 panic。它是导致 级联故障 的主要原因。如果一个 error 产生了,函数必须返回 error 并且允许调用者决定是否处理它。

Bad Good
func foo(bar string) {
if len(bar) == {
panic("bar must not be empty")
}
// ...
} func main() {
if len(os.Args) != {
fmt.Println("USAGE: foo <bar>")
os.Exit()
}
foo(os.Args[])
}
func foo(bar string) error {
if len(bar) ==
return errors.New("bar must not be empty")
}
// ...
return nil
} func main() {
if len(os.Args) != {
fmt.Println("USAGE: foo <bar>")
os.Exit()
}
if err := foo(os.Args[]); err != nil {
panic(err)
}
}

panic/recover 不是 error 处理策略。程序在发生不可恢复的时候会产生 panic,例如对 nil 进行解引用。一个例外是在程序初始化的时候:在程序启动时那些可能终止程序的问题会造成 panic。

var _statusTemplate = template.Must(template.New("name").Parse("_statusHTML"))
甚至在测试用例中,更偏向于使用 t.Fatal 或者 t.FailNow 解决 panic 确保这个测试被标记为失败。
Bad Good
// func TestFoo(t *testing.T)

f, err := ioutil.TempFile("", "test")
if err != nil {
panic("failed to set up test")
}
// func TestFoo(t *testing.T)

f, err := ioutil.TempFile("", "test")
if err != nil {
t.Fatal("failed to set up test")
}

使用 go.uber.org/atomic

使用 sync/atomic 对原生类型(例如,int32int64)进行原子操作的时候,很容易在读取或者修改变量的时候忘记使用原子操作。

go.uber.org/atomic 通过隐藏底层类型使得这些操作是类型安全的。此外,它还包含一个比较方便的 atomic.Bool 类型。

Bad Good
type foo struct {
running int32 // atomic
} func (f* foo) start() {
if atomic.SwapInt32(&f.running, ) == {
// already running…
return
}
// start the Foo
} func (f *foo) isRunning() bool {
return f.running == // race!
}
type foo struct {
running atomic.Bool
} func (f *foo) start() {
if f.running.Swap(true) {
// already running…
return
}
// start the Foo
} func (f *foo) isRunning() bool {
return f.running.Load()
}

性能

指定的性能指南仅适用于 hot path(译者注:hot path 指频繁执行的代码路径)

strconv 优于 fmt

对基本数据类型的字符串表示的转换,strconv 比 fmt 速度快。

Bad Good
var i int = ...
s := fmt.Sprint(i)
var i int = ...
s := strconv.Itoa(i)

避免 string 到 byte 的转换

不要重复用固定 string 创建 byte slice。相反,执行一次转换后保存结果,避免重复转换。

Bad Good
for i := ; i < b.N; i++ {
w.Write([]byte("Hello world"))
}
data := []byte("Hello world")
for i := ; i < b.N; i++ {
w.Write(data)
}
BenchmarkBad-4   50000000   22.2 ns/op
BenchmarkGood-4  500000000   3.25 ns/op

代码风格

聚合相似的声明

Go 支持分组声明。

Bad Good
import "a"
import "b"
import (
"a"
"b"
)

也能应用于常量,变量和类型的声明。

Bad Good
const a =
const b = var a =
var b = type Area float64
type Volume float64
const (
a =
b =
) var (
a =
b =
) type (
Area float64
Volume float64
)

只需要对相关类型进行分组声明。不相关的不需要进行分组声明。

Bad Good
type Operation int

const (
Add Operation = iota +
Subtract
Multiply
ENV_VAR = "MY_ENV"
)
type Operation int

const (
Add Operation = iota +
Subtract
Multiply
) const ENV_VAR = "MY_ENV"

分组不受限制。例如,我们可以在函数内部使用它们。

Bad Good
func f() string {
var red = color.New(0xff0000)
var green = color.New(0x00ff00)
var blue = color.New(0x0000ff) ...
}
func f() string {
var (
red = color.New(0xff0000)
green = color.New(0x00ff00)
blue = color.New(0x0000ff)
) ...
}

包的分组导入的顺序

有两个导入分组:

  • 标准库
  • 其他库

这是默认情况下 goimports 应用的分组。

Bad Good
import (
"fmt"
"os"
"go.uber.org/atomic"
"golang.org/x/sync/errgroup"
)
import (
"fmt"
"os" "go.uber.org/atomic"
"golang.org/x/sync/errgroup"
)

包命名

当给包命名的时候,可以参考以下方法,

  • 都是小写字母。没有大写字母或者下划线
  • 在大多数场景下没必要重命名包
  • 简明扼要。记住,每次调用时都会通过名称来识别。
  • 不要复数。例如,要使用 net/url, 不要使用 net/urls
  • 不要使用 "common", "util", "shared", "lib" 诸如此类的命名。这种方式不太好,无法从名字中获取有效信息。

也可以参考 Package Names 和 Style guideline for Go packages.

函数命名

我们遵循 Go 社区的习惯方法,使用驼峰法命名函数。测试函数是个例外,包含下划线是为了分组相关的测试用例。例如,TestMyFunction_WhatIsBeingTested

别名导入

如果包名和导入路径的最后一个元素不匹配,则要使用别名导入。

import (
"net/http" client "example.com/client-go"
trace "example.com/trace/v2"
)

在大部分场景下,除非导入的包有直接的冲突,应该避免使用别名导入。

Bad Good
import (
"fmt"
"os" nettrace "golang.net/x/trace"
)
import (
"fmt"
"os"
"runtime/trace" nettrace "golang.net/x/trace"
)

函数分组和顺序

  • 函数应该按大致的调用顺序排序
  • 同一个文件的函数应该按接收者分组

因此,导出的函数应该在 structconstvar 定义之后。

newXYZ()/NewXYZ() 应该在类型定义之后,并且在接收者的其余的方法之前出现。

因为函数是按接收者分组的,所以普通的函数应该快到文件末尾了。

Bad Good
func (s *something) Cost() {
return calcCost(s.weights)
} type something struct{ ... } func calcCost(n int[]) int {...} func (s *something) Stop() {...} func newSomething() *something {
return &something{}
}
type something struct{ ... }

func newSomething() *something {
return &something{}
} func (s *something) Cost() {
return calcCost(s.weights)
} func (s *something) Stop() {...} func calcCost(n int[]) int {...}

减少嵌套

在可能的情况下,代码应该通过先处理 错误情况/特殊条件 并提前返回或继续循环来减少嵌套。

Bad Good
for _, v := range data {
if v.F1 == {
v = process(v)
if err := v.Call(); err == nil {
v.Send()
} else {
return err
}
} else {
log.Printf("Invalid v: %v", v)
}
}
for _, v := range data {
if v.F1 != {
log.Printf("Invalid v: %v", v)
continue
} v = process(v)
if err := v.Call(); err != nil {
return err
}
v.Send()
}

不必要的 else

如果在 if 的两个分支中都设置同样的变量,则可以用单个 if 替换它。

Bad Good
var a int
if b {
a =
} else {
a =
}
a :=
if b {
a =
}

顶层变量的声明

在顶层,使用标准的 var 关键字。不要指定类型,除非它与表达式的类型不同。

Bad Good
var _s string = F()

func F() string { return "A" }
var _s = F()
// F 已经声明了返回一个 string,我们不需要再次指定类型 func F() string { return "A" }

如果表达式的类型与请求的类型不完全匹配,请指定类型。

type myError struct{}

func (myError) Error() string { return "error" }

func F() myError { return myError{} }

var _e error = F()
// F 返回了一个 myError 类型的对象,但是我们想要 error

在不可导出的全局变量前面加上 _

在不可导出的顶层 var 和 const 的前面加上 _,以便明确它们是全局符号。

特例:不可导出的 error 值前面应该加上 err 前缀。

理论依据:顶层变量和常量有一个包作用域。使用通用的名称很容易在不同的文件中意外地使用错误的值

Bad Good
// foo.go

const (
defaultPort =
defaultUser = "user"
) // bar.go func Bar() {
defaultPort :=
...
fmt.Println("Default port", defaultPort) // 我们将 Bar() 的第一行删除,将不会看到编译错误
}
// foo.go

const (
_defaultPort =
_defaultUser = "user"
)

结构体的嵌入

嵌入的类型(例如 mutex)应该在结构体字段的头部,并且在嵌入字段和常规字段间保留一个空行来隔离。

Bad Good
type Client struct {
version int
http.Client
}
type Client struct {
http.Client version int
}

使用字段名去初始化结构体

当初始化结构体的时候应该指定字段名称,现在在使用 go vet 的情况下是强制性的。

Bad Good
k := User{"John", "Doe", true}
k := User{
FirstName: "John",
LastName: "Doe",
Admin: true,
}

特例:当有 3 个或更少的字段时,可以在测试表中省略字段名。

tests := []struct{
}{
op Operation
want string
}{
{Add, "add"},
{Subtract, "subtract"},
}

局部变量声明

短变量声明(:=)应该被使用在有明确值的情况下。

Bad Good
var s = "foo"
s := "foo"

然而,使用 var 关键字在某些情况下会让默认值更清晰,声明空 Slice,例如

Bad Good
func f(list []int) {
filtered := []int{}
for _, v := range list {
if v > {
filtered = append(filtered, v)
}
}
}
func f(list []int) {
var filtered []int
for _, v := range list {
if v > {
filtered = append(filtered, v)
}
}
}

nil 是一个有效的 slice

nil 是一个长度为 0 的 slice。意思是,

  • 使用 nil 来替代长度为 0 的 slice 返回

    Bad Good
    if x == "" {
    return []int{}
    }
    if x == "" {
    return nil
    }
  • 检查一个空 slice,应该使用 len(s) == 0,而不是 nil

    Bad Good
    func isEmpty(s []string) bool {
    return s == nil
    }
    func isEmpty(s []string) bool {
    return len(s) ==
    }
  • The zero value (a slice declared with var) is usable immediately without make().

  • 零值(通过 var 声明的 slice)是立马可用的,并不需要 make() 。

    Bad Good
    nums := []int{}
    // or, nums := make([]int) if add1 {
    nums = append(nums, )
    } if add2 {
    nums = append(nums, )
    }
    var nums []int
    
    if add1 {
    nums = append(nums, )
    } if add2 {
    nums = append(nums, )
    }

减少变量的作用域

在没有 减少嵌套 相冲突的情况下,尽量减少变量的作用域。

Bad Good
err := f.Close()
if err != nil {
return err
}
if err := f.Close(); err != nil {
return err
}

如果在 if 之外需要函数调用的结果,则不要缩小作用域。

Bad Good
if f, err := os.Open("f"); err == nil {
_, err = io.WriteString(f, "data")
if err != nil {
return err
}
return f.Close()
} else {
return err
}
f, err := os.Open("f")
if err != nil {
return err
} if _, err := io.WriteString(f, "data"); err != nil {
return err
} return f.Close()

避免裸参数

函数调用中的裸参数不利于可读性。当参数名的含义不明显时,添加 C 语言风格的注释(/*…*/)。

Bad Good
// func printInfo(name string, isLocal, done bool)

printInfo("foo", true, true)
// func printInfo(name string, isLocal, done bool)

printInfo("foo", true /* isLocal */, true /* done */)

更好的方法是,用自定义类型替换裸 bool 类型,以获得更可读的和类型安全的代码。这使得该参数未来的状态是可以增加的,不仅仅是两种(true/false)。

type Region int

const (
UnknownRegion Region = iota
Local
) type Status int const (
StatusReady = iota +
StatusDone
// 可能未来我们将有一个 StatusInProgress 的状态
) func printInfo(name string, region Region, status Status)

使用原生字符串格式来避免转义

Go 支持 原生字符串格式 ,它可以跨越多行并包含引号。使用这些来避免手动转义的字符串,因为手动转义的可读性很差。

Bad Good
wantError := "unknown name:\"test\""
wantError := `unknown error:"test"`

初始化结构体

在初始化结构体的时候使用 &T{} 替代 new(T),以至于结构体初始化是一致的。

Bad Good
sval := T{Name: "foo"}

// 不一致
sptr := new(T)
sptr.Name = "bar"
sval := T{Name: "foo"}

sptr := &T{Name: "bar"}

在 Printf 之外格式化字符串

如果你在 Printf 风格函数的外面声明一个格式化字符串,请使用 const 值。

这有助于 go vet 对格式化字符串执行静态分析。

Bad Good
msg := "unexpected values %v, %v\n"
fmt.Printf(msg, , )
const msg = "unexpected values %v, %v\n"
fmt.Printf(msg, , )

Printf-style 函数的命名

当你声明一个 Printf 风格的函数,请确认 go vet 能够发现并检查这个格式化字符串。

这意味着你应该尽可能为 Printf 风格的函数名进行预定义 。go vet 默认会检查它们。查看 Printf family 获取更多信息。

如果预定义函数名不可取,请用 f 作为名字的后缀即 wrapf,而不是 wrapgo vet 可以检查特定的 printf 风格的名称,但它们必须以 f 结尾。

$ go vet -printfuncs=wrapf,statusf

请参考 go vet: Printf family check

设计模式

表格驱动测试

当核心测试逻辑重复的时候,用 subtests 做表格驱动测试(译者注:table-driven tests 即 TDT 表格驱动方法)可以避免重复的代码。

Bad Good
// 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, "", 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, "", port) host, port, err = net.SplitHostPort("1:8")
require.NoError(t, err)
assert.Equal(t, "", host)
assert.Equal(t, "", port)
// 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: "",
},
{
give: "192.0.2.0:http",
wantHost: "192.0.2.0",
wantPort: "http",
},
{
give: ":8000",
wantHost: "",
wantPort: "",
},
{
give: "1:8",
wantHost: "",
wantPort: "",
},
} 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)
})
}

表格驱动测试使向错误消息添加上下文、减少重复逻辑和添加新测试用例变得更容易。

我们遵循这样一种约定,即结构体 slice 被称为 tests,每个测试用例被称为 tt。此外,我们鼓励使用 give 和 want前缀解释每个测试用例的输入和输出值。

tests := []struct{
give string
wantHost string
wantPort string
}{
// ...
} for _, tt := range tests {
// ...
}

函数参数可选化

函数参数可选化(functional options)是一种模式,在这种模式中,你可以声明一个不确定的 Option 类型,该类型在内部结构体中记录信息。函数接收可选化的参数,并根据在结构体上记录的参数信息进行操作

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

Bad Good
// package db

func Connect(
addr string,
timeout time.Duration,
caching bool,
) (*Connection, error) {
// ...
} // timeout 和 caching 必须要提供,哪怕用户想使用默认值 db.Connect(addr, db.DefaultTimeout, db.DefaultCaching)
db.Connect(addr, newTimeout, db.DefaultCaching)
db.Connect(addr, db.DefaultTimeout, false /* caching */)
db.Connect(addr, newTimeout, false /* caching */)
type options struct {
timeout time.Duration
caching bool
} // Option 重写 Connect.
type Option interface {
apply(*options)
} type optionFunc func(*options) func (f optionFunc) apply(o *options) {
f(o)
} func WithTimeout(t time.Duration) Option {
return optionFunc(func(o *options) {
o.timeout = t
})
} func WithCaching(cache bool) Option {
return optionFunc(func(o *options) {
o.caching = cache
})
} // Connect 创建一个 connection
func Connect(
addr string,
opts ...Option,
) (*Connection, error) {
options := options{
timeout: defaultTimeout,
caching: defaultCaching,
} for _, o := range opts {
o.apply(&options)
} // ...
} // Options 只在需要的时候提供 db.Connect(addr)
db.Connect(addr, db.WithTimeout(newTimeout))
db.Connect(addr, db.WithCaching(false))
db.Connect(
addr,
db.WithCaching(false),
db.WithTimeout(newTimeout),
)

请参考,

GO代码风格指南 Uber Go (转载)的更多相关文章

  1. 《Google 代码风格指南》

    <Google 代码风格指南> https://github.com/google/styleguide

  2. python的PEP8 代码风格指南

    PEP8 代码风格指南 这篇文章原文实际上来自于这里:https://www.python.org/dev/peps/pep-0008/ 知识点 代码排版 字符串引号 表达式和语句中的空格 注释 版本 ...

  3. C++代码风格指南总结

    C++代码风格指南 代码风格的重要性 今天我收到thougthwork笔试没过的消息, 心里确实很难受, 然后师兄说我代码写得很糟糕 细想一下, 我写代码确实是随心所欲, 并没有遵循什么规范; 所以现 ...

  4. Google JavaScript代码风格指南

    Google JavaScript代码风格指南 修正版本 2.28 Aaron Whyte Bob Jervis Dan Pupius Eric Arvidsson Fritz Schneider R ...

  5. 大神的JS代码风格指南

    js代码风格指南:1.缩进使用空格,不要用制表符2.必须用分号3.暂时不用ES6(modules)例如export和import命令4.不鼓励(不禁止)水平对齐5.少用var 都应该使用const或者 ...

  6. 读 Angular 代码风格指南

    读 Angular 代码风格指南 本文写于 2021 年 1 月 17 日 原文地址:Angular 文档 该文章拥有完整的代码风格指南--大到如何编排文件夹,小到如何进行变量命名都涉及.但是与 ng ...

  7. python代码风格指南:pep8 中文翻译

    摘要 本文给出主Python版本标准库的编码约定.CPython的C代码风格参见​PEP7.本文和​PEP 257 文档字符串标准改编自Guido最初的<Python Style Guide&g ...

  8. python代码风格指南:pep8 中文版

    本文档所提供的编码规范,适用于主要的Python发行版中组成标准库的Python代码.请参阅PEP关于Python的C实现的C编码风格指南的描述. 本文档和PEP257(文档字符串规范)改编自Guid ...

  9. Google代码风格指南

    官网:https://github.com/google/styleguide 中文版:https://github.com/zh-google-styleguide/zh-google-styleg ...

随机推荐

  1. redis 持久化RDB、AOF

    1.redis持久化简介 Redis是一种高级key-value数据库.它跟memcached类似,不过数据可以持久化,而且支持的数据类型很丰富.有字符串,链表,集合和有序集合.支持在服务器端计算集合 ...

  2. VS2019 C++动态链接库的创建使用(3) - 如何导出类

    如何在动态链接库里导出一个类? ①在库头文件里增加一个类声明,class DLL1_API Point是将类内所有成员都导出,如果只导出某个成员函数,则只需在对应的成员函数前加DLL1_API即可: ...

  3. 在一台Linux服务器上安装多个MySQL实例(一)--使用mysqld_multi方式

    (一)MySQL多实例概述 实例是进程与内存的一个概述,所谓MySQL多实例,就是在服务器上启动多个相同的MySQL进程,运行在不同的端口(如3306,3307,3308),通过不同的端口对外提供服务 ...

  4. 给社团同学做的R语言爬虫分享

    大家好,给大家做一个关于R语言爬虫的分享,很荣幸也有些惭愧,因为我是一个编程菜鸟,社团里有很多优秀的同学经验比我要丰富的多,这次分享是很初级的,适用于没有接触过爬虫且有一些编程基础的同学,内容主要有以 ...

  5. TensorFlow 一步一步实现卷积神经网络

    欢迎大家关注我们的网站和系列教程:http://www.tensorflownews.com/,学习更多的机器学习.深度学习的知识! TensorFlow 从入门到精通系列教程: http://www ...

  6. 字典树模板 HDU - 1251

    题意: 给一些单词,换行键后,查找以后输入的单词作为前缀的话们在之前出现过几次. 思路: 字典树模板----像查字典的顺序一样 #include<string> #include<s ...

  7. vscode vue 格式化 ESLint 检查 单引号 双引号 函数括号报错问题

    vscode vue 格式化 最近重新搞了下电脑,重装了 vscode 软件,在用 vue 写项目的时候,照例开启了 ESLint 语法检查,但是发现在使用 vscode 格式化快捷键的时候(shif ...

  8. 工作日志,Excel导入树结构数据

    目录 1. 前言 2. 需求分析 2.1 需求难点 2.2 解决难点 2.3 表格设计 3. 功能实现 3.1 一个分枝 3.2 一个分枝多个树叶 3.3 多个分枝多个树叶 4. 代码事例 4.1 目 ...

  9. 《java编程思想》 初始化与清理

    1.初始化与清理的重要性: 1.许多C程序的错误都源于程序员忘记初始化变量,特别是使用程序库时,如果不知道如何初始化库的构件更容易出错 2.当使用完一个元素时,这个元素就不会有什么影响了,所以很容易就 ...

  10. Docker容器入门-基本命令的使用

    目前容器技术使用相当广泛 不会或者没有使用过容器感觉都不像是个搞技术的 所以,我也就docker相关内容做一个整理 只有不断的学习,才能保持自己的竞争力 什么是容器? 容器是一种轻量级.可移植.自包含 ...