Go语言开发最佳实践

Go语言作为C语言家族的年轻一员,最初Google的设计者站在前人的肩膀上,其目标就是能够给程序员带来快乐、匹配未来硬件发展趋势,且适合开发大型软件系统。
其核心思想就是:

  • 追求简单,少即是多
  • 偏好组合,正交解耦
  • 原生并发,轻量高效
  • 面向工程,自带电池

一些最佳实践:

1. 使用gofmt格式化代码

当代码开发人员还在争论不同代码风格时,Rob Pike告诉你:gofmt的代码风格不是某个人的最爱,而是所有人的最爱。通过这种”独裁”的方式,终结了关于 代码风格好坏的争论。因此团队开发中,提交代码前一定记得使用gofmt格式化代码。


2. 命名相关

  • 包名使用小写字母
  • 包名尽量与导入路径最后一个路径分段保持一直
  • 变量名使用小驼峰命名法,如需导出使用大驼峰
  • 变量声明与使用的距离尽量短
  • 常量使用大写字母

3. 变量声明的方式

推荐声明聚类和就近,对于局部逻辑需要使用的变量,他们之间可能存在一定关联,可以如下一起声明。

var (
	v1 int64
	v2 string
	v3 []int
)

延迟初始化,使用var关键字:

var val int
for i := 0; i < 10; i++ {
	if vals[i] > target {
		val = i
    }
}

声明且显式初始化,使用短变量声明形式:

val1 := 12
val2 := "test"

4. 保持零值可用

var s []int
s = append(s, 1)
s = append(s, 2)  

如上代码段,声明之后没有初始化就使用,在很多语言中是报错的,但是在go的设计中,是提倡零值可用的。原生类型都有默认零值。 我们在定义自己的类型时也推荐零值可用。但是也要注意,零值并不是所有操作都可用,对于以上代码,如果初始化后根据下表取值,那么就会报错了。


5. 使用复合字面值作为初始化构造器

如下所示,初始化一个结构体:

var s Student
s.Name = "Jack"
s.Age = 20
s.Address = "xxx"
...

实际上更推荐以下方式:

s := &Student{
	Name : "Jack",
	Age : 20,
	Address : "xxx",
	...
}

常用的集合如切片、map都有类似的简化方式,表达能力更强且书写简单。


6. 正确使用切片

和其他编程语言一样,go语言也有数组类型,不同于C语言,go中数组变量并不是指向第一个元素的指针,而是代表整个数组,因此作为函数参数 传递时,会进行值拷贝,这个性能开销是很大的。日常开发中,更多场景是使用切片,数组更多的是作为底层存储结构,同时,切片的动态扩容也是 日常使用必不可少的能力。
我们经常使用切片的append操作向切片中添加元素,但需要特别注意元素长度超过底层数组容量后产生的数据复制,因此推荐使用切片时提前初始化 合适的容量,这将显著提升程序性能。

s := make([]int64, len, cap) // 提前确定cap

7. 正确使用map

  • map的key应该严格定义了==!=两个操作符的行为,因此函数、map、切片不能作为key使用;
  • 习惯使用comma ok:
if value,ok := m["key"]; ok {
	// key存在map中
}
  • 不要依赖遍历map得到的元素顺序,这是不确定的。
  • map并发写时不安全的,并发场景可以使用sync.Map类型。
  • 和切片一样,由于扩容有成本,推荐初始化map传入容量。

8. 遵循快乐路径原则

这不是go语言独有的,是一种通用的编程原则,比较一下一目了然:

\\ bad 
if condition1 {
	if condition2 {
		if condition3 {
			// do something
}
}
}

\\ good   
if !conditon1 {
	return
}
if !condition2 {
	return
}
if !condition3 {
	return
}
// do something

9. 使用for-range避坑

  • 小心迭代变量重用
// wrong 
for i,v := range mylist {
	go func() {
		time.Sleep(time.Second * 3)
		fmt.PrintLn(i, v)
}
// ...

// right
for i,v := range mylist {
    go func(i, v int) {
        time.Sleep(time.Second * 3)
        fmt.PrintLn(i, v)
    }(i, v)
}

10. 注意defer关键字后表达式的求值时机

defer后面表达式的求值是在将函数注册到延迟堆栈的时候。
注意以下函数的输出区别:

func foo1() {
	for i := 0; i <= 3; i++ {
		defer fmt.Println(i)
	}
}

func foo2() {
	for i := 0; i <= 3; i++ {
		defer func(n int) {
			fmt.Println(n)
		}(i)
	}
}

func foo3() {
	for i := 0; i <= 3; i++ {
		defer func() {
			fmt.Println(i)
		}()
	}
}

11. 选择合适的的方法receiver类型

type T struct {
	a int
}

func (t T) M1() {
}

func (t *T) M2() {
	t.a = 11
}   

以上两种receiver方式都可以,需要注意

  • 如果要对实例做修改,需要使用指针类型
  • 需要考虑对象size,对象复制有性能损耗

12. 推荐面向接口编程

这条原则也是语言无关的编程思路,构建大型软件系统,都需要有这种思路。
go语言中,自定义类型与接口的关系是松耦合的,即某个类型实现了接口的所有方法集合,那么就认为该类型实现了接口, 不需要显式使用implements

type XXXService interface {
	Open()
	Close()
}

type MyXXXService struct {}

func (s *XXXService) Open() {
	// open
}

func (s *XXXService) Close() {
    // close
}

13. 组合大于继承

偏好组合是go的设计哲学之一,使用类型嵌入实现OOP领域的继承机制。
类型嵌入包括:

  • 接口类型中嵌入接口类型
type Interface1 interface {
	M1()
}

type Interface2 interface {
	M1()
	M2()
}

type Interface3 interface {
	Interface1
	Interface2
}

type Interface4 interface {
	Interface2
	M2()
}
  • 结构体中嵌入接口类型
type Interface interface {
	M1()
	M2()
}

type T struct {
	Interface
}
  • 结构体中嵌入结构体
type T1 struct{}

func (T1) T1M1()   { println("T1's M1") }
func (T1) T1M2()   { println("T1's M2") }
func (*T1) PT1M3() { println("PT1's M3") }

type T2 struct{}

func (T2) T2M1()   { println("T2's M1") }
func (T2) T2M2()   { println("T2's M2") }
func (*T2) PT2M3() { println("PT2's M3") }

type T struct {
	T1
	*T2
}

14. 不建议使用函数重载

很多编程语言支持函数重载(overload),go设计者认为实践中会造成混淆和脆弱,因此不支持函数重载,但是go支持可变参数列表, 一定程度上可以实现函数重载,但实际生产中不建议使用。

15. 推荐Option模式

实际开发中有这样的场景,尝试将函数的参数设置成可选项。可以使用一些已经设置默认配置和开箱即用的对象, 同时您也可以使用一些更为详细的配置,例如初始化一个客户端,提供了超时时间、日志开关、重试策略等默认值, 但使用方也需要自由设置。这种场景推荐使用Option模式。

var defaultStuffClientOptions = StuffClientOptions{
    Retries: 3,
    Timeout: 2,
}
type StuffClientOption func(*StuffClientOptions)
type StuffClientOptions struct {
    Retries int //number of times to retry the request before giving up
    Timeout int //connection timeout in seconds
}
func WithRetries(r int) StuffClientOption {
    return func(o *StuffClientOptions) {
        o.Retries = r
    }
}
func WithTimeout(t int) StuffClientOption {
    return func(o *StuffClientOptions) {
        o.Timeout = t
    }
}
type StuffClient interface {
    DoStuff() error
}
type stuffClient struct {
    conn    Connection
    timeout int
    retries int
}
type Connection struct {}
func NewStuffClient(conn Connection, opts ...StuffClientOption) StuffClient {
    options := defaultStuffClientOptions
    for _, o := range opts {
        o(&options)
    }
        return &stuffClient{
            conn:    conn,
            timeout: options.Timeout,
            retries: options.Retries,
        }
}
func (c stuffClient) DoStuff() error {
    return nil
}

16. nil error判断避坑

先看如下代码:

type MyError struct {
	error
}

var ErrBad = MyError{
	error: errors.New("bad error"),
}

func bad() bool {
	return false
}

func returnsError() error {
	var p *MyError = nil
	if bad() {
		p = &ErrBad
	}
	return p
}

func main() {
	e := returnsError()
	if e != nil {
		fmt.Printf("error: %+v\n", e)
		return
	}
	fmt.Println("ok")
}

需要注意这里returnError返回的error不是nil,原因在于,error是一个接口,接口类型变量 的数据结构包含了接口类型信息以及赋值给该接口的动态类型信息,因此这里返回的动态类型信息虽然为空, 但接口类型信息是有的,因此返回值并不是nil

17. 适配器函数类型

适配器模式通常用于接口转换和兼容,是一种常用设计模式。
比如有一个People类型和一个SayHello接口:

type People struct {
	name string
}

func (p *People) Greet() {
	fmt.Printf("Hello, I am %s.\n", p.name)
}

type Hello interface {
	SayHello()
}

func SayHello(s Hello) {
	s.SayHello()
}

如果要将People原有的Greet方法适配到SayHello接口,通常我们可以定义一个新的类型作为适配器,

type PeopleSayHello struct {
	*People
}

func (p *PeopleSayHello) SayHello() {
	p.Greet()
}

对于go来说,函数和其他类型一样都属于“一等公民”,其他类型能够实现接口,函数也可以。因此 就有另一种实现方式,即为函数定义方法:

type Hello interface {
	SayHello()
}

func SayHello(s Hello) {
	s.SayHello()
}

// 适配器
type PeopleSayHello func()

func (f PeopleSayHello) SayHello() {
	f()
}

func main() {
    p = People{"Adam"}
	SayHello(PeopleSayHello(p.Greet))
}

这样就可以将Greet方法适配到SayHello接口,这种方式并不常见,特别是对于熟悉javaC++ 语言的开发者,第一种方式更加通用易理解。

如果觉得我的文章对您有用,请在支付宝公益平台找个项目捐点钱。 @sxzhou Nov 5, 2022

奉献爱心