说在前面 本文提供了一些 Golang 工程项目的最佳实践,分别从可读性、健壮性和效率三个方面进行描述。
Readable 可读性对于机器来说其实没有什么区别,但是对于作为开发者的每一个人
来说,可读性就显得极其重要了。对于个人开发者来说,一个人不可能记住他写的每一行代码,很多时候我们自己写的代码,经过两三个月之后,我们基本上就会忘记当初写的逻辑,高可读的代码可以让我们更加轻易地进行阅读回忆;而对于协同开发来说,代码的可读性直接决定了多人合作开发的效率,高可读的代码可以让我们协作开发的效率得到巨大的提升,那么常用的代码中有哪些方式可以提升代码的可读性呢?
if,else and happy path Talk is cheap,show me the code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func abs (x int ) { if x >= 0 { return x } else { return -x } } func abs (x int ) { if x >= 0 { return x } return -x }
上面这个例子是去掉不必要的 else,这样可以让我们的代码可读性更高,此外公司 gitlab CI 的时候 lint 会自动检测出这个问题,辅助我们写出更加可读的代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 func aFunc () error { err := doSomething() if err == nil { err := doAnotherThing() if err == nil { return nil } return err } return err } func aFunc () error { err := doSomething() if err != nil { return err } err := doAnotherThing() if err != nil { return err } return nil }
显而易见,第二种代码可读性比第一种要高得多,这就是尽早返回错误的写法,同时也消除了 happy path,让我们的代码变得更加的清晰易懂
1 2 3 4 5 6 7 8 9 10 11 12 var a intif flag { a = 1 } else { a = -1 } var a = -1 if flag { a = 1 }
进行类似 bool 判断后赋值操作时,我们可以提前分配默认值,提升可读性
init() 我们尽量不要使用 init() 函数来进行赋值和初始化等操作,因为 init() 函数在包被引用时就会自动调用,可能会产生意想不到的副作用
1 2 3 4 5 6 7 package tiktokclientsfunc init () { tiktokclient = buildNewTiktokClient() ... }
上述的写法会在每一个即使不想 build tiktokclient 的引用 tiktokclients 包的地方都调用一次 init() 方法,导致 tiktokclient 不断的 build,从而产生问题
1 2 3 4 5 6 7 package tiktokclientsfunc InitClient () { tiktokclient = buildNewTiktokClient() ... }
使用显示的初始化方法,需要初始化的地方才进行初始化,从而避免 init() 函数的副作用。
注释可以辅助我们更好的理解代码,但是不好的注释反而可能起到反作用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func Abs(num int ) int { if num < 0 { reutrn -num } return num } func Abs(num int ) int { if num < 0 { reutrn -num } return num }
注释不用解释函数里面每一个逻辑是什么样的,直接解释这个函数是什么作用就可以了。
对于我们而言,其实写出可读性更加好的代码的意义要比写注释更加大,因为注释可能会过时,而代码一定是最新的,如果改了代码却没有更新注释,很可能会误导阅读代码的人,所以加强我们的代码可读性是更加重要的。
Robust 健壮的代码指的是我们的代码发生错误之后,我们的程序不至于崩溃而还能继续执行。在 Golang 中,最重要的就是处理 panic 和 error。
panic 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func main () { fmt.Println ("start") panic (errors.New("this is a panic)) p := recover () fmt.Print ("panic: %v" , p) fmt.Println ("end" ) } func main () { defer func () { p := recover () fmt.Print ("panic: %v" , p) } fmt.Println ("start") panic (errors.New("this is a panic)) fmt.Println ("end") }
发生 panic 的函数会直接抛出 panic 而不会继续运行之后的代码,所以为了捕获 panic,我们需要在函数开头使用 defer 匿名函数来保证可以捕获到 panic
error 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 func main () { e := io.EOF fmt.Println (e == io.EOF) e = fmt.Errorf ("contenxt: %w" , io.EOF) fmt.Println (e == io.EOF) } func main () { e := io.EOF fmt.Println (errors.Is (e, io.EOF)) e = fmt.Errorf ("contenxt: %w" , io.EOF) fmt.Println (errors.Is (e, io.EOF)) } func main () { _, err := os.Open ("non-existing" ) _, ok := err.(*fs.PathError) fmt.Println (ok) e := fmt.Errorf ("contenxt: %w" , err) _, ok := err.(*fs.PathError) fmt.Println (ok) } func main () { _, err := os.Open ("non-existing" ) var pathError *fs.PathError fmt.Println (errors.As (err, &pathError) e := fmt.Errorf ("contenxt: %w" , err) fmt.Println (errors.As (e, &pathError) }
在 Golang 的 1.13 版本之前对于 err 的处理可能会出现一些不符合直觉的错误,比如 fmt.Errorf 拼接之后的错误无法溯源和正确转化等,此时我们可以使用 errors.Is() 和 errors.As() 方法解决此类问题。
Efficient 关于效率,可以使用指针,因为传递指针的时候不会发生值的 copy,可以节省资源提高性能,那么在哪些情况下我们应当使用指针或不使用指针呢? |Good|Bad| |-|-| |当方法内部需要修改传递进来的参数时|指针指向一个 interface| |需要传递一个很大的数据时|方法内部不需要修改传递进来的参数时| |为了保持代码一致性,都使用了指针时|需要传递的数据很小时|
指针的坑 使用指针是还有一个坑,就是如果在循环中使用指针,会导致循环时取到的值是同一个
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 func main () { x := []int {1 ,2 ,3 } r := make ([]*int , 0 , len (x)) for _, v := range x { r = append (r, &v) } for _, v := range r { fmt.Print(*v) } } func main () { x := []int {1 ,2 ,3 } r := make ([]*int , 0 , len (x)) for _, v := range x { c := v r = append (r, &c) } for _, v := range r { fmt.Print(*v) } }
第二种写法也是标准的循环中使用 goroutine 时的写法,先用一个局部变量承接循环的值,再将局部变量传递到 goroutine 中。
deprecation 我们的代码是不断更新的,当旧的方法不再使用时,我们需要在其上添加 Deprecated
注释,以表示不再建议使用,在 Goland 等 IDE 中还会根据这个注释进行相应的强提示,告诉我们使用更新的方法。