武汉的开发者们注意啦!AI技术战略、框架以及最佳实战尽在Azure OpenAI Day 了解详情
写点什么

Uber 正式开源 Go 语言编程规范,内部已使用多年

  • 2019-10-17
  • 本文字数:21350 字

    阅读完需:约 70 分钟

Uber正式开源Go语言编程规范,内部已使用多年

近日,Uber 开源了其公司内部使用的《Go 语言编程规范》。该指南是为了使代码库更易于管理,同时让工程师有效地使用 Go 语言特性。文档中详细描述了在 Uber 编写 Go 代码的各种注意事项,包括具体的“Dos and Don’ts of writing Go code at Uber”,也就是 Go 代码应该怎样写、不该怎样写。今天我们就来简单了解一下国外大厂都是如何来写代码的。行文仓促,错误之处,多多指正。

1. 介绍

英文原文标题是 Uber Go Style Guide,这里的 Style 是指在管理我们代码的时候可以遵从的一些约定。


这篇编程指南的初衷是更好的管理我们的代码,包括去编写什么样的代码,以及不要编写什么样的代码。我们希望通过这份编程指南,代码可以具有更好的维护性,同时能够让我们的开发同学更高效地编写 Go 语言代码。


这份编程指南最初由 Prashant VaranasiSimon Newton 编写,旨在让其他同事快速地熟悉和编写 Go 程序。经过多年发展,现在的版本经过了多番修改和改进了。这是我们在 Uber 遵从的编程范式,但是很多都是可以通用的,如下是其他可以参考的链接:



所有的提交代码都应该通过 golintgo vet 检测,建议在代码编辑器上面做如下设置:


  • 保存的时候运行 goimports

  • 使用 golintgo vet 去做错误检测。


你可以通过下面链接发现更多的 Go 编辑器的插件: https://github.com/golang/go/wiki/IDEsAndTextEditorPlugins

2. 编程指南

2.1 指向 Interface 的指针

在我们日常使用中,基本上不会需要使用指向 interface 的指针。当我们将 interface 作为值传递的时候,底层数据就是指针。Interface 包括两方面:


  • 一个包含 type 信息的指针

  • 一个指向数据的指针


如果你想要修改底层的数据,那么你只能使用 pointer。

2.2 Receiver 和 Interface

使用值作为 receiver 的时候 method 可以通过指针调用,也可以通过值来调用。


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{1: {"A"}}
// You can only call Read using a valuesVals[1].Read()
// This will not compile:// sVals[1].Write("test")
sPtrs := map[int]*S{1: {"A"}}
// You can call both Read and Write using a pointersPtrs[1].Read()sPtrs[1].Write("test")
复制代码


相似的,pointer 也可以满足 interface 的要求,尽管 method 使用 value 作为 receiver。


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 Fi = s1Vali = s1Ptri = s2Ptr
// The following doesn't compile, since s2Val is a value, and there is no value receiver for f.// i = s2Val
复制代码


Effective Go 关于如何使用指针和值也有一些不错的 practice: Pointers vs. Values.

2.3 mutex 默认 0 值是合法的

sync.Mutexsync.RWMutex 的 0 值也是合法的,所以我们基本不需要声明一个指针指向 mutex。


Bad


mu := new(sync.Mutex)mu.Lock()
复制代码


Good


var mu sync.Mutexmu.Lock()
复制代码


如果 struct 内部使用 mutex,在我们使用 struct 的指针类型时候,mutex 也可以是一个非指针类型的 field,或者直接嵌套在 struct 中。


Mutex 直接嵌套在 struct 中。


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]}
复制代码


将 Mutex 作为一个 struct 内部一个非指针类型 Field 使用。


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]}
复制代码

2.4 拷贝 Slice 和 Map

Slice 和 Map 都包含了对底层存储数据的指针,所以注意在修改 slice 或者 map 数据的场景下,是不是使用了引用。

slice 和 map 作为参数

当把 slice 和 map 作为参数的时候,如果我们对 slice 或者 map 的做了引用操作,那么修改会修改掉原始值。如果这种修改不是预期的,那么要先进行 copy。


Bad


func (d *Driver) SetTrips(trips []Trip) {  d.trips = trips}
trips := ...d1.SetTrips(trips)
// Did you mean to modify d1.trips?trips[0] = ...
复制代码


Good


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[0] = ...
复制代码
slice 和 map 作为返回值

当我们的函数返回 slice 或者 map 的时候,也要注意是不是直接返回了内部数据的引用到外部。


Bad


type Stats struct {  sync.Mutex
counters map[string]int}
// Snapshot returns the current stats.func (s *Stats) Snapshot() map[string]int { s.Lock() defer s.Unlock()
return s.counters}
// snapshot is no longer protected by the lock!snapshot := stats.Snapshot()
复制代码


Good


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 is now a copy.snapshot := stats.Snapshot()
复制代码

2.5 使用 defer 做资源清理

建议使用 defer 去做资源清理工作,比如文件,锁等。


Bad


p.Lock()if p.count < 10 {  p.Unlock()  return p.count}
p.count++newCount := p.countp.Unlock()
return newCount
// easy to miss unlocks due to multiple returns
复制代码


Good


p.Lock()defer p.Unlock()
if p.count < 10 { return p.count}
p.count++return p.count
// more readable
复制代码


尽管使用 defer 会导致一定的性能开销,但是大部分情况下这个开销在你的整个链路上所占的比重往往是微乎其微,除非说真的是有非常高的性能需求。另外使用 defer 带来的代码可读性的改进以及减少代码发生错误的概率都是值得的。

2.6 channel 的 size 最好是 1 或者是 unbuffered

在使用 channel 的时候,最好将 size 设置为 1 或者使用 unbuffered channel。其他 size 的 channel 往往都会引入更多的复杂度,需要更多考虑上下游的设计。


Bad


// Ought to be enough for anybody!c := make(chan int, 64)
复制代码


Good


// Size of onec := make(chan int, 1) // or// Unbuffered channel, size of zeroc := make(chan int)
复制代码

2.7 枚举变量应该从 1 开始

在 Go 语言中枚举值的声明典型方式是通过 constiota 来声明。由于 0 是默认值,所以枚举值最好从一个非 0 值开始,比如 1。


Bad


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


Good


type Operation int
const ( Add Operation = iota + 1 Subtract Multiply)
// Add=1, Subtract=2, Multiply=3
复制代码


有一种例外情况:0 值是预期的默认行为的时候,枚举值可以从 0 开始。


type LogOutput int
const ( LogToStdout LogOutput = iota LogToFile LogToRemote)
// LogToStdout=0, LogToFile=1, LogToRemote=2
复制代码

2.8 Error 类型

在 Go 语言中声明 error 可以有多种方式:


  • errors.New 声明包含简单静态字符串的 error

  • fmt.Errorf 格式化 error string

  • 其他自定义类型使用了 Error() 方法

  • 使用 "pkg/errors".Wrap


当要把 error 作为返回值的时候,可以考虑如下的处理方式


  • 是不是不需要额外信息,如果是,errors.New 就足够了。

  • client 需要检测和处理返回的 error 吗?如果是,最好使用实现了 Error() 方法的自定义类型,这样可以包含更多的信息。

  • error 是不是从下游函数传递过来的?如果是,考虑一下 error wrap,参考:section on error wrapping.

  • 其他情况,fmt.Errorf 一般足够了。


对于 client 需要检测和处理 error 的情况,这里详细说一下。如果你要通过 errors.New 声明一个简单的 error,那么可以使用一个变量声明:var ErrCouldNotOpen = errors.New("Could not open")


Bad


// 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") } }}
复制代码


Good


// 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 中包含更多的信息,而不仅仅类型原生 error 的这种简单字符串,那么最好使用一个自定义类型。


Bad


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") } }}
复制代码


Good


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 类型的时候,最好 export 配套的检测自定义 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") }}
复制代码

2.9 Error Wrapping

在函数调用失败的时候,有三种方式可以将下游的 error 传递出去:


  • 直接返回失败函数返回的 error。

  • 使用 "pkg/errors".Wrap 增加更多的上下文信息,这种情况下可以使用 "pkg/errors".Cause 去提取原始的 error 信息。

  • 如果调用者不需要检测和处理返回的 error 信息的话,可以直接使用 fmt.Errorf 将需要附加的信息进行格式化添加进去。


如果条件允许,最好增加上下文信息。比如 “connection refused” 和 “call service foo: connection refused” 这两种错误信息在可读性上比较也是高下立判。当增加上下文信息的时候,尽量保持简洁。比如像 “failed to” 这种极其明显的信息就没有必要写上去了。


Bad


s, err := store.New()if err != nil {    return fmt.Errorf(        "failed to create new store: %s", err)}
复制代码


Good


s, err := store.New()if err != nil {    return fmt.Errorf(        "new store: %s", err)}
复制代码


另外对于需要传播到其他系统的 error,也要有明显的标识信息,比如在 log 的最前面增加 err 等字样。


更多参考:Don’t just check errors, handle them gracefully.

2.10 类型转换失败处理

类型转换失败会导致进程 panic,所以对于类型转换,一定要使用 “comma ok” 的范式来处理。


Bad


t := i.(string)
复制代码


Good


t, ok := i.(string)if !ok {  // handle the error gracefully}
复制代码

2.11 不要 panic

对于线上环境要尽量避免 panic。在很多情况下,panic 都是引起雪崩效应的罪魁祸首。一旦 error 发生,我们应该向上游调用者返回 error,并且容许调用者对 error 进行检测和处理。


Bad


func foo(bar string) {  if len(bar) == 0 {    panic("bar must not be empty")  }  // ...}
func main() { if len(os.Args) != 2 { fmt.Println("USAGE: foo <bar>") os.Exit(1) } foo(os.Args[1])}
复制代码


Good


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


Panic/Recover 并不是一种 error 处理策略。进程只有在某些不可恢复的错误发生的时候才需要 panic。


在跑 test case 的时候,使用 t.Fatal 或者 t.FailNow ,而不是 panic 来保证这个 test case 会被标记为失败的。


Bad


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


Good


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

2.12 使用 go.uber.org/atomic

这个是 Uber 内部对原生包 sync/atomic 的一种封装,隐藏了底层数据类型。


Bad


type foo struct {  running int32  // atomic}
func (f* foo) start() { if atomic.SwapInt32(&f.running, 1) == 1 { // already running… return } // start the Foo}
func (f *foo) isRunning() bool { return f.running == 1 // race!}
复制代码


Good


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()}
复制代码

3. 性能相关

3.1 类型转换时,使用 strconv 替换 fmt

当基本类型和 string 互转的时候,strconv 要比 fmt 快。


Bad


for i := 0; i < b.N; i++ {  s := fmt.Sprint(rand.Int())}
BenchmarkFmtSprint-4 143 ns/op 2 allocs/op
复制代码


Good


for i := 0; i < b.N; i++ {  s := strconv.Itoa(rand.Int())}
BenchmarkStrconv-4 64.2 ns/op 1 allocs/op
复制代码

3.2 避免 string to byte 的不必要频繁转换

在通过 string 创建 byte slice 的时候,不要在循环语句中重复的转换,而是要将重复的转换逻辑提到循环外面,做一次即可。(看上去很 general 的建议)


Bad


for i := 0; i < b.N; i++ {  w.Write([]byte("Hello world"))}
BenchmarkBad-4 50000000 22.2 ns/op
复制代码


Good


data := []byte("Hello world")for i := 0; i < b.N; i++ {  w.Write(data)}
BenchmarkGood-4 500000000 3.25 ns/op
复制代码

4. 编程风格

4.1 声明语句分组

import 语句分组


Bad


import "a"import "b"
复制代码


Good


import (  "a"  "b")
复制代码


常量、变量以及 type 声明


Bad


const a = 1const b = 2
var a = 1var b = 2
type Area float64type Volume float64
复制代码


Good


const (  a = 1  b = 2)
var ( a = 1 b = 2)
type ( Area float64 Volume float64)
复制代码


import 根据导入的包进行顺序分组。(其他库我们其实可以再细分 private 库和 public 库)


  • 标准库

  • 其他库


Bad


import (  "fmt"  "os"  "go.uber.org/atomic"  "golang.org/x/sync/errgroup")
复制代码


Good


import (  "fmt"  "os"
"go.uber.org/atomic" "golang.org/x/sync/errgroup")
复制代码

4.2 package 命名

package 命名的几条规则:


  • 全小写。不包含大写字母或者下划线。

  • 简洁。

  • 不要使用复数。比如,使用 net/url,而不是 net/urls

  • 避免:“common”, “util”, “shared”, “lib”,不解释。


更多参考:


4.3 函数命名

函数命名遵从社区规范: MixedCaps for function names 。有一种特例是 TestCase 中为了方便测试做的函数命名,比如:TestMyFunction_WhatIsBeingTested

4.4 import 别名

当 package 的名字和 import 的 path 的最后一个元素不同的时候,必须要起别名。


import (  "net/http"
client "example.com/client-go" trace "example.com/trace/v2")
复制代码


另外,import 别名要尽量避免,只要在不得不起别名的时候再这么做,比如避免冲突。


Bad


import (  "fmt"  "os"
nettrace "golang.net/x/trace")
复制代码


Good


import (  "fmt"  "os"  "runtime/trace"
nettrace "golang.net/x/trace")
复制代码

4.5 函数分组和排序

  • 函数应该按调用顺序排序

  • 一个文件中的函数应该按 receiver 排序


newXYZ/NewXYZ 最好紧接着类型声明后面,并在其他的 receiver 函数前面。


Bad


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{}}
复制代码


Good


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 {...}
复制代码

4.6 避免代码块嵌套

优先处理异常情况,快速返回,避免代码块过多嵌套。看下面代码会比较直观。


Bad


for _, v := range data {  if v.F1 == 1 {    v = process(v)    if err := v.Call(); err == nil {      v.Send()    } else {      return err    }  } else {    log.Printf("Invalid v: %v", v)  }}
复制代码


Good


for _, v := range data {  if v.F1 != 1 {    log.Printf("Invalid v: %v", v)    continue  }
v = process(v) if err := v.Call(); err != nil { return err } v.Send()}
复制代码

4.7 避免不必要的 else 语句

很多情况下,if - else 语句都能通过一个 if 语句表达,比如如下代码。


Bad


var a intif b {  a = 100} else {  a = 10}
复制代码


Good


a := 10if b {  a = 100}
复制代码

4.8 两级 (two-level) 变量声明

所有两级变量声明就是一个声明的右值来自另一个表达式,这个时候第一级变量声明就不需要指明类型,除非这两个地方的数据类型不同。看代码会更直观一点。


Bad


var _s string = F()
func F() string { return "A" }
复制代码


Good


var _s = F()// Since F already states that it returns a string, we don't need to specify// the type again.
func F() string { return "A" }
复制代码


上面说的第二种两边数据类型不同的情况。


type myError struct{}
func (myError) Error() string { return "error" }
func F() myError { return myError{} }
var _e error = F()// F returns an object of type myError but we want error.
复制代码

4.9 对于不做 export 的全局变量使用前缀 _

对于同一个 package 下面的多个文件,一个文件中的全局变量可能会被其他文件误用,所以建议使用 _ 来做前缀。(其实这条规则有待商榷)


Bad


// foo.go
const ( defaultPort = 8080 defaultUser = "user")
// bar.go
func Bar() { defaultPort := 9090 ... fmt.Println("Default port", defaultPort)
// We will not see a compile error if the first line of // Bar() is deleted.}
复制代码


Good


// foo.go
const ( _defaultPort = 8080 _defaultUser = "user")
复制代码

4.10 struct 嵌套

struct 中的嵌套类型在 field 列表排在最前面,并且用空行分隔开。


Bad


type Client struct {  version int  http.Client}
复制代码


Good


type Client struct {  http.Client
version int}
复制代码

4.11 struct 初始化的时候带上 Field

这样会更清晰,也是 go vet 鼓励的方式


Bad


k := User{"John", "Doe", true}
复制代码


Good


k := User{    FirstName: "John",    LastName: "Doe",    Admin: true,}
复制代码

4.12 局部变量声明

变量声明的时候可以使用 := 以表示这个变量被显示的设置为某个值。


Bad


var s = "foo"
复制代码


Good


s := "foo"
复制代码


但是对于某些情况使用 var 反而表示的更清晰,比如声明一个空的 slice: Declaring Empty Slices


Bad


func f(list []int) {  filtered := []int{}  for _, v := range list {    if v > 10 {      filtered = append(filtered, v)    }  }}
复制代码


Good


func f(list []int) {  var filtered []int  for _, v := range list {    if v > 10 {      filtered = append(filtered, v)    }  }}
复制代码

4.13 nil 是合法的 slice

在返回值是 slice 类型的时候,直接返回 nil 即可,不需要显式地返回长度为 0 的 slice。


Bad


if x == "" {  return []int{}}
复制代码


Good


if x == "" {  return nil}
复制代码


判断 slice 是不是空的时候,使用 len(s) == 0


Bad


func isEmpty(s []string) bool {  return s == nil}
复制代码


Good


func isEmpty(s []string) bool {  return len(s) == 0}
复制代码


使用 var 声明的 slice 空值可以直接使用,不需要 make()


Bad


nums := []int{}// or, nums := make([]int)
if add1 { nums = append(nums, 1)}
if add2 { nums = append(nums, 2)}
复制代码


Good


var nums []int
if add1 { nums = append(nums, 1)}
if add2 { nums = append(nums, 2)}
复制代码

4.14 避免 scope

Bad


err := ioutil.WriteFile(name, data, 0644)if err != nil { return err}
复制代码


Good


if err := ioutil.WriteFile(name, data, 0644); err != nil { return err}
复制代码


当然某些情况下,scope 是不可避免的,比如


Bad


if data, err := ioutil.ReadFile(name); err == nil {  err = cfg.Decode(data)  if err != nil {    return err  }
fmt.Println(cfg) return nil} else { return err}
复制代码


Good


data, err := ioutil.ReadFile(name)if err != nil {   return err}
if err := cfg.Decode(data); err != nil { return err}
fmt.Println(cfg)return nil
复制代码

4.15 避免参数语义不明确(Avoid Naked Parameters)

Naked Parameter 指的应该是意义不明确的参数,这种情况会破坏代码的可读性,可以使用 C 分格的注释(/*...*/)进行注释。


Bad


// func printInfo(name string, isLocal, done bool)
printInfo("foo", true, true)
复制代码


Good


// func printInfo(name string, isLocal, done bool)
printInfo("foo", true /* isLocal */, true /* done */)
复制代码


对于上面的示例代码,还有一种更好的处理方式是将上面的 bool 类型换成自定义类型。


type Region int
const ( UnknownRegion Region = iota Local)
type Status int
const ( StatusReady = iota + 1 StatusDone // Maybe we will have a StatusInProgress in the future.)
func printInfo(name string, region Region, status Status)
复制代码

4.16 使用原生字符串,避免转义

Go 支持使用反引号,也就是 “`” 来表示原生字符串,在需要转义的场景下,我们应该尽量使用这种方案来替换。


Bad


wantError := "unknown name:\"test\""
复制代码


Good


wantError := `unknown error:"test"`
复制代码

4.17 Struct 引用初始化

使用 &T{} 而不是 new(T) 来声明对 T 类型的引用,使用 &T{} 的方式我们可以和 struct 声明方式 T{} 保持统一。


Bad


sval := T{Name: "foo"}
// inconsistentsptr := new(T)sptr.Name = "bar"
复制代码


Good


sval := T{Name: "foo"}
sptr := &T{Name: "bar"}
复制代码

4.18 字符串 string format

如果我们要在 Printf 外面声明 format 字符串的话,使用 const,而不是变量,这样 go vet 可以对 format 字符串做静态分析。


Bad


msg := "unexpected values %v, %v\n"fmt.Printf(msg, 1, 2)
复制代码


Good


const msg = "unexpected values %v, %v\n"fmt.Printf(msg, 1, 2)
复制代码

4.19 Printf 风格函数命名

当声明 Printf 风格的函数时,确保 go vet 可以对其进行检测。可以参考:Printf family


另外也可以在函数名字的结尾使用 f 结尾,比如: WrapF,而不是 Wrap。然后使用 go vet


$ go vet -printfuncs=wrapf,statusf
复制代码


更多参考: go vet: Printf family check.

5. 编程模式(Patterns)

5.1 Test Tables

当测试逻辑是重复的时候,通过 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,都会显得更加的清晰。


在命名方面,我们将 struct 的 slice 命名为 tests,同时每一个 test case 命名为 tt。而且,我们强烈建议通过 givewant 前缀来表示 test case 的 input 和 output 的值。


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

5.2 Functional Options

关于 functional options 简单来说就是通过类似闭包的方式来进行函数传参。


Bad


// package db
func Connect( addr string, timeout time.Duration, caching bool,) (*Connection, error) { // ...}
// Timeout and caching must always be provided,// even if the user wants to use the default.
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 */)
复制代码


Good


type options struct {  timeout time.Duration  caching bool}
// Option overrides behavior of 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 creates a connection.func Connect( addr string, opts ...Option,) (*Connection, error) { options := options{ timeout: defaultTimeout, caching: defaultCaching, }
for _, o := range opts { o.apply(&options) }
// ...}
// Options must be provided only if needed.
db.Connect(addr)db.Connect(addr, db.WithTimeout(newTimeout))db.Connect(addr, db.WithCaching(false))db.Connect( addr, db.WithCaching(false), db.WithTimeout(newTimeout),)
复制代码


更多参考:



注:关于 functional option 这种方式我本人也强烈推荐,我很久以前也写过一篇类似的文章,感兴趣的可以移步: 写扩展性好的代码:函数

6. 总结

Uber 开源的这个文档,通篇读下来给我印象最深的就是:保持代码简洁,并具有良好可读性。不得不说,相比于国内很多 “代码能跑就完事了” 这种写代码的态度,这篇文章或许可以给我们更多的启示和参考。


2019-10-17 07:0010377

评论 3 条评论

发布
用户头像
xxx

Bad

2021-10-20 18:00
回复
用户头像
受益匪浅!
2019-10-20 14:47
回复
用户头像
相比于国内很多 “代码能跑就完事了” 这种写代码的态度,这篇文章或许可以给我们更多的启示和参考。

这句说的真好。我记得阿里也发布了Java的编程指南,不知道还有没有其他公司有开放类似的东西~
2019-10-17 10:30
回复
没有更多了
发现更多内容

小伙伴面经分享京东+面试八股文整套面试真题(含答案)

钟奕礼

Java 程序员 java面试 java编程

测试自动化中遵循的最佳实践

禅道项目管理

自动化测试

个推TechDay治数训练营直播预告 | 从方法论到落地应用,详解企业标签体系建设要点

个推

标签 用户画像 标签体系

个推发布《Android13适配指南》,解读Android13新特性

个推

android 安卓 安卓开发

Go语言—big包的使用

良猿

Go golang 后端 11月月更 goweb

待办事项是什么意思,为什么要用?

优秀

待办事项

解密金融行业数据云平台建设密码

数造万象

LigaAI X 猴子无限 | AIGC火了,人类又得到了什么?

LigaAI

分布式 大模型训练 研发协作平台 AIGC 大规模离散模型

这份1658页的Java面试核心突击讲,成功让我上岸阿里

小二,上酒上酒

Java 程序员 面试 阿里 大厂面试

推荐5款IDEA插件,堪称代码质量检查利器!

程序员小毕

Java 程序员 程序人生 后端 IDEA

阿里P8大佬总结的Nacos入门笔记,从安装到进阶小白也能轻松学会

小二,上酒上酒

Java 编程 程序员 nacos

Java岗史上最全八股文面试真题汇总,堪称2022年面试天花板

小二,上酒上酒

Java 程序员 面试 八股文

阿里云张建锋:核心云产品全面 Serverless 化

Serverless Devs

ShareSDK for Flutter

MobTech袤博科技

工业物联网DCS和SCADA的区别

2D3D前端可视化开发

物联网 DCS web组态软件 SCADA 工业组态

CSS 基础属性篇组成及作用

千锋IT教育

EMR-StarRocks 与 Flink 在汇量实时写入场景的最佳实践

阿里云大数据AI技术

数据库 flink EMR 十一月月更

提升汽车APP用户体验,火山引擎APMPlus的“独家秘笈”

字节跳动终端技术

性能监控 APP开发 应用性能 火山引擎 APMPlus

KnowStreaming贡献流程

石臻臻的杂货铺

kafka 后端 11月月更

2022最新整理上千道Java面试攻略,近500页PDF文档

钟奕礼

Java Java 面试 java程序员 java编程

微服务调用的正确打开方式

Java全栈架构师

Java 程序员 面试 微服务 后端

破坏系统是为了更稳定?混沌工程在去哪儿的4个阶段实践

TakinTalks稳定性社区

混沌工程 故障治理

数据治理必读|基于Dataphin,快速建设高质量数据支撑业务发展

瓴羊企业智能服务

手把手教你成为荣耀开发者:商户服务开通指南

荣耀开发者服务平台

android 开发者 手机 荣耀 honor

Baklib知识库|为什么知识共享工具对减少内部知识缺口至关重要

Baklib

知识

又一创新!阿里云 Serverless 调度论文被云计算顶会 ACM SoCC 收录

阿里巴巴云原生

阿里云 Serverless 云原生

阿里大牛纯手写的微服务入门笔记,从基础到进阶直接封神

小二,上酒上酒

Java 编程 程序员 架构 微服务

在线电子表格,助力数据分析人员高效办公

流量猫猫头

大数据

星策转型大咖说第二弹!前喜茶数字化副总裁、前百果科技首席技术市场官沈欣老师数字化转型经验分享!

星策开源社区

开源 方法论 转型 智能化转型

2022年11月《中国数据库行业分析报告》重磅发布!精彩抢先看

墨天轮

人工智能 数据库 dba 智能运维 国产数据库

自学 UI 设计有哪些书籍推荐

千锋IT教育

Uber正式开源Go语言编程规范,内部已使用多年_AICon_legendtkl_InfoQ精选文章