Effective Go 要点速记
13 min read

Effective Go 要点速记

关于 Effective Go 中要点的总结,可以帮助理解 Go 语言本身,也可以快速恢复对 Go 语言的记忆。
Effective Go 要点速记
Photo by SHVETS
💡
「要写好 Go 了解它的属性和习语很重要,了解 Go 编程的既定约定也很重要,例如命名、格式、程序构造等,以便编写的程序易于其他 Go 程序员理解」,这是 Effective Go 这份文档的初衷,但是 Effective Go 是于 2009 年编写发布的,此后没有进行重大更新。虽然它是理解如何使用语言本身的一个很好的指南,但由于语言的稳定性,它很少提到库,也没有提到自编写以来 Go 生态系统的重大变化,例如构建系统、测试、模块和多态性。所以,Effective Go 仍然有用,但需要清楚的是它远非完整的指南。

1. 代码格式

运行 gofmt 工具进行 go 代码的标准格式化,vs code 可以自己运行具体配置参考:Go with Visual Studio Code

几个 go 代码格式习惯:

  • Indentation,go 采用 Tab 进行缩紧,除非必要的时候才使用空格;
  • line length,go 没有行长度限制,如果行太长为了可读性可以 Tab 缩进将其包起来;
  • parentheses,go 少用括号包括 for、if、switch 等,另外操作符优先级通过空格长短控制,如x<<8 + y<<16

2. 代码注释

  • 块注释:/* */
  • 行注释://
  • 所有代码之前的没有换行的注释被认为是文档自声明,例如 /* Copyright 2023 Alibaba.Inc */

3. 命名

3.1 package 命名

通常 package 名是小写字母、单字名、不采用下划线和大小写混合。由于 package name 的存在,我们在命名导出成员(exported names)的时候有一些注意点:

  • 例如 package 导出的 reader 类型,命名为 Reader 就比较清晰简洁不需要 BufReader,因为在引用的时候是 bufio.Reader 这样就清晰的表达了,bufio.BufReader 就略显重复;
  • 例如 package 导出的构造函数命名,以 ring 包的构造函数为例 ring.NewRing()ring.Ring(),、ring.New(),New 命名就清晰简洁的;

3.2 getter&setter 命名

go 没有 getter,setter 装饰器,以 obj.Owner 为例,通常会有类似 SetOwner 和 GetOwner,但是以根据命名清晰简洁的原则可以命名为:obj.Owner(), obj.SetOwner()

3.3 interface 命名

  • 只有一个 method 的 interface 的命名可以是 method name + "er",例如 Reader
  • 采用规范、有特殊含义的命名,例如 Read, Write, Close, Flush, String 等

4. 分号

go 不像 C 那样需要分号作为分隔,由此带来的问题 if、for、switch、select 这种控制结构的括号不能换行:

if i < f() {

}

if i < f() // wrong
{          // wrong

}

5. 控制结构

5.1 if

if x > 0 {
	return y;
}

if err := file.Chmod(0644); err != nil {
	fmt.Println(err)
	return err
}

f, err := os.Open(filename)
if err != nil {
	return err
} // no need else
d, err := f.Stat() // reassignment and redeclatation
if err != nil {
	f.Clos()
	return err
}

5.2 for

// Like a C for
for init; condition; post { }

// Like a C while
for condition { }

// Like a C for(;;)
for { }

// normal for loop
sum := 0
for i := 0; i < 10; i++ {
    sum += i
}

// array
for key, value := range oldMap {
    newMap[key] = value
}
// drop the second
for key := range m {
    if key.expired() {
        delete(m, key)
    }
}
// blank identifier
sum := 0
for _, value := range array {
    sum += value
}

5.3 switch

func unhex(c byte) byte {
    switch {
    case '0' <= c && c <= '9':
        return c - '0'
    case 'a' <= c && c <= 'f':
        return c - 'a' + 10
    case 'A' <= c && c <= 'F':
        return c - 'A' + 10
    }
    return 0
}

func shouldEscape(c byte) bool {
    switch c {
    case ' ', '?', '&', '=', '#', '+', '%':
        return true
    }
    return false
}

for n := 0; n < len(src); n += size {
    switch {
    case src[n] < sizeOne:
        if validateOnly {
            break
        }
        size = 1
        update(src[n])
    case src[n] < sizeTwo:
        if n+1 >= len(src) {
            err = errShortInput
            break Loop
        }
        if validateOnly {
            break
        }
        size = 2
        update(src[n] + src[n+1]<<shift)
    }
}

// Compare returns an integer comparing the two byte slices,
// lexicographically.
// The result will be 0 if a == b, -1 if a < b, and +1 if a > b
func Compare(a, b []byte) int {
    for i := 0; i < len(a) && i < len(b); i++ {
        switch {
        case a[i] > b[i]:
            return 1
        case a[i] < b[i]:
            return -1
        }
    }
    switch {
    case len(a) > len(b):
        return 1
    case len(a) < len(b):
        return -1
    }
    return 0
}

var t interface{}
t = functionOfSomeType()
switch t := t.(type) {
default:
    fmt.Printf("unexpected type %T\n", t)     // %T prints whatever type t has
case bool:
    fmt.Printf("boolean %t\n", t)             // t has type bool
case int:
    fmt.Printf("integer %d\n", t)             // t has type int
case *bool:
    fmt.Printf("pointer to boolean %t\n", *t) // t has type *bool
case *int:
    fmt.Printf("pointer to integer %d\n", *t) // t has type *int
}

6. 函数

6.1 多返回值

func nextInt(b []byte, i int) (int, int) {
    for ; i < len(b) && !isDigit(b[i]); i++ {
    }
    x := 0
    for ; i < len(b) && isDigit(b[i]); i++ {
        x = x*10 + int(b[i]) - '0'
    }
    return x, i
}

for i := 0; i < len(b); {
    x, i = nextInt(b, i)
    fmt.Println(x)
}

6.2 返回值命名

func ReadFull(r Reader, buf []byte) (n int, err error) {
    for len(buf) > 0 && err == nil {
        var nr int
        nr, err = r.Read(buf)
        n += nr
        buf = buf[nr:]
    }
    return
}

6.3 defer

// Contents returns the file's contents as a string.
func Contents(filename string) (string, error) {
    f, err := os.Open(filename)
    if err != nil {
        return "", err
    }
    defer f.Close()  // f.Close will run when we're finished.

    var result []byte
    buf := make([]byte, 100)
    for {
        n, err := f.Read(buf[0:])
        result = append(result, buf[0:n]...) // append is discussed later.
        if err != nil {
            if err == io.EOF {
                break
            }
            return "", err  // f will be closed if we return here.
        }
    }
    return string(result), nil // f will be closed if we return here.
}

// defer 遵循 LIFO

7. 数据

7.1 new

new 给分配内存但是不做初始化,new(T) 为类型 T 的变量分配全0的存储空间并返回其地址,返回类型是 *T

type SyncedBuffer struct {
    lock    sync.Mutex
    buffer  bytes.Buffer
}

p := new(SyncedBuffer)  // type *SyncedBuffer
var v SyncedBuffer      // type  SyncedBuffer

// 构造函数增强 new 初始化
func NewFile(fd int, name string) *File {
    if fd < 0 {
        return nil
    }
    f := new(File)
    f.fd = fd
    f.name = name
    f.dirinfo = nil
    f.nepipe = 0
    return f
}

7.2 make

make 为特性类型做初始化,它们包括:map、slice、channel,make 初始化的内存不是全0,返回的类型是 T

make([]int, 10, 100)

make(chan, int)

// Allocate the top-level slice.
picture := make([][]uint8, YSize) // One row per unit of y.
// Loop over the rows, allocating the slice for each row.
for i := range picture {
    picture[i] = make([]uint8, XSize)
}

m := make(map[string]int)
m["k1"] = 7

7.3 array

go array 就是一个固定长度的 slice,go 的数组与 c 的数组有几点区别:

  • Arrays are values. Assigning one array to another copies all the elements.
  • In particular, if you pass an array to a function, it will receive a copy of the array, not a pointer to it.
  • The size of an array is part of its type. The types [10]int and[20]int are distinct.

7.4 slice

切片包装阵列可为数据序列提供更通用,功能强大和方便的接口。除了具有明确维度(例如变换矩阵)的项目外,Go 中的大多数数组编程都是用切片而不是简单的数组完成的。

var buf []byte

n, err := f.Read(buf[0:32])
var n int
var err error
for i := 0; i < 32; i++ {
    nbytes, e := f.Read(buf[i:i+1])  // Read one byte.
    n += nbytes
    if nbytes == 0 || e != nil {
        err = e
        break
    }
}

7.5 two-dimensional slice

type Transform [3][3]float64  // A 3x3 array, really an array of arrays.
type LinesOfText [][]byte     // A slice of byte slices.

7.6 maps

Maps are a convenient and powerful built-in data structure that associate values of one type (the key) with values of another type (the element or value).

var timeZone = map[string]int{
    "UTC":  0*60*60,
    "EST": -5*60*60,
    "CST": -6*60*60,
    "MST": -7*60*60,
    "PST": -8*60*60,
}
// test if the map has the member
value, present := timeZone[tz]
if present {
	return True
}

7.7 print

fmt.Printf("Hello %d\n", 23)
fmt.Fprint(os.Stdout, "Hello ", 23, "\n")
fmt.Println("Hello", 23)
fmt.Println(fmt.Sprint("Hello ", 23))
fmt.Printf("%v\n", timeZone)  // or just fmt.Println(timeZone)

type T struct {
    a int
    b float64
    c string
}
t := &T{ 7, -2.35, "abc\tdef" }
fmt.Printf("%v\n", t)
fmt.Printf("%+v\n", t)
fmt.Printf("%#v\n", t)
fmt.Printf("%#v\n", timeZone)
/* output:
&{7 -2.35 abc   def}
&{a:7 b:-2.35 c:abc     def}
&main.T{a:7, b:-2.35, c:"abc\tdef"}
map[string]int{"CST":-21600, "EST":-18000, "MST":-25200, "PST":-28800, "UTC":0}
 */

7.8 append

append 函数原型:

func append(slice []T, elements ...T) []T

// append example
x := []int{1,2,3}
x = append(x, 4, 5, 6)
fmt.Println(x)

8. 初始化

8.1 constant

Go 中使用 iota 进行枚举的自动初始化创建:

type ByteSize float64

const (
    _           = iota // ignore first value by assigning to blank identifier
    KB ByteSize = 1 << (10 * iota)
    MB
    GB
    TB
    PB
    EB
    ZB
    YB
)

func (b ByteSize) String() string {
    switch {
    case b >= YB:
        return fmt.Sprintf("%.2fYB", b/YB)
    case b >= ZB:
        return fmt.Sprintf("%.2fZB", b/ZB)
    case b >= EB:
        return fmt.Sprintf("%.2fEB", b/EB)
    case b >= PB:
        return fmt.Sprintf("%.2fPB", b/PB)
    case b >= TB:
        return fmt.Sprintf("%.2fTB", b/TB)
    case b >= GB:
        return fmt.Sprintf("%.2fGB", b/GB)
    case b >= MB:
        return fmt.Sprintf("%.2fMB", b/MB)
    case b >= KB:
        return fmt.Sprintf("%.2fKB", b/KB)
    }
    return fmt.Sprintf("%.2fB", b)
}
// 这里使用 Sprintf("%f") 避免了使用 format string 引发的无限递归的问题,format string 转换成 string 的时候会调用 String()

8.2 vars

var (
    home   = os.Getenv("HOME")
    user   = os.Getenv("USER")
    gopath = os.Getenv("GOPATH")
)

8.3 init 函数

每个源码文件都可以定义一个 init 函数,init 函数没有参数(niladic init function)。另外一个关键点 init 函数的执行时机:在包中的所有变量声明都对其初始化器求值之后调用Init,并且只有在所有导入的包都初始化之后才对这些初始化器求值。

func init() {
    if user == "" {
        log.Fatal("$USER not set")
    }
    if home == "" {
        home = "/home/" + user
    }
    if gopath == "" {
        gopath = home + "/go"
    }
    // gopath may be overridden by --gopath flag on command line.
    flag.StringVar(&gopath, "gopath", gopath, "override default GOPATH")
}

9. 方法

9.1 指针与值

Go 默认是值传递,除了指针和 interface。

type ByteSlice []byte

// value receiver
func (slice ByteSlice) Append(data []byte) []byte {
    // Body exactly the same as the Append function defined above.
}

// pointer receiver
func (p *ByteSlice) Append(data []byte) {
    slice := *p
    // Body as above, without the return.
    *p = slice
}

9.2 interface

interface 在 Go 用来为某个对象 object 指定行为。

type Sequence []int

// Methods required by sort.Interface.
func (s Sequence) Len() int {
    return len(s)
}
func (s Sequence) Less(i, j int) bool {
    return s[i] < s[j]
}
func (s Sequence) Swap(i, j int) {
    s[i], s[j] = s[j], s[i]
}

// Copy returns a copy of the Sequence.
func (s Sequence) Copy() Sequence {
    copy := make(Sequence, 0, len(s))
    return append(copy, s...)
}

// Method for printing - sorts the elements before printing.
func (s Sequence) String() string {
    s = s.Copy() // Make a copy; don't overwrite argument.
    sort.Sort(s)
    str := "["
    for i, elem := range s { // Loop is O(N²); will fix that in next example.
        if i > 0 {
            str += " "
        }
        str += fmt.Sprint(elem)
    }
    return str + "]"
}

9.3 类型转换

type Stringer interface {
    String() string
}

var value interface{} // Value provided by caller.
switch str := value.(type) {
case string:
    return str
case Stringer:
    return str.String()
}

// type assertion
str, ok := value.(string)
if ok {
    fmt.Printf("string value is: %q\n", str)
} else {
    fmt.Printf("value is not a string\n")
}

10. blank identifier

blank identifier 可以是任意类型任意值。

if _, err := os.Stat(path); os.IsNotExist(err) {
    fmt.Printf("%s does not exist\n", path)
}
package main

import (
    "fmt"
    "io"
    "log"
    "os"
)

// unused imports and variables
var _ = fmt.Printf // For debugging; delete when done.
var _ io.Reader    // For debugging; delete when done.

func main() {
    fd, err := os.Open("test.go")
    if err != nil {
        log.Fatal(err)
    }
    // TODO: use fd.
    _ = fd
}
// import for side affects without any explicit use
import _ "net/http/pprof"
// interface check
if _, ok := val.(json.Marshaler); ok {
    fmt.Printf("value %v of type %T implements json.Marshaler\n", val, val)
}

11. embedding

// embedding type
type Job struct {
    Command string
    *log.Logger
}

job := &Job{command, log.New(os.Stderr, "Job: ", log.Ldate)}

// use embedding type direct
func (job *Job) Printf(format string, args ...interface{}) {
    job.Logger.Printf("%q: %s", job.Command, fmt.Sprintf(format, args...))
}

12. concurrency

12.1 goroutine

go list.Sort()  // run list.Sort concurrently; don't wait for it.

func Announce(message string, delay time.Duration) {
    go func() {
        time.Sleep(delay)
        fmt.Println(message)
    }()  // Note the parentheses - must call the function.
}

12.2 channel

ci := make(chan int)            // unbuffered channel of integers
cj := make(chan int, 0)         // unbuffered channel of integers
cs := make(chan *os.File, 100)  // buffered channel of pointers to Files


c := make(chan int)  // Allocate a channel.
// Start the sort in a goroutine; when it completes, signal on the channel.
go func() {
    list.Sort()
    c <- 1  // Send a signal; value does not matter.
}()
doSomethingForAWhile()
<-c   // Wait for sort to finish; discard sent value.


var sem = make(chan int, MaxOutstanding)

func handle(r *Request) {
    sem <- 1    // Wait for active queue to drain.
    process(r)  // May take a long time.
    <-sem       // Done; enable next request to run.
}

func Serve(queue chan *Request) {
    for {
        req := <-queue
        go handle(req)  // Don't wait for handle to finish.
    }
}

12.3 channels of channels

type Request struct {
    args        []int
    f           func([]int) int
    resultChan  chan int
}

/* cleint */
func sum(a []int) (s int) {
    for _, v := range a {
        s += v
    }
    return
}
request := &Request{[]int{3, 4, 5}, sum, make(chan int)}
// Send request
clientRequests <- request
// Wait for response.
fmt.Printf("answer: %d\n", <-request.resultChan)

/* server */
func handle(queue chan *Request) {
    for req := range queue {
        req.resultChan <- req.f(req.args)
    }
}

12.4 并行

type Vector []float64

// Apply the operation to v[i], v[i+1] ... up to v[n-1].
func (v Vector) DoSome(i, n int, u Vector, c chan int) {
    for ; i < n; i++ {
        v[i] += u.Op(v[i])
    }
    c <- 1    // signal that this piece is done
}



const numCPU = 4 // number of CPU cores
func (v Vector) DoAll(u Vector) {
    c := make(chan int, numCPU)  // Buffering optional but sensible.
    for i := 0; i < numCPU; i++ {
        go v.DoSome(i*len(v)/numCPU, (i+1)*len(v)/numCPU, u, c)
    }
    // Drain the channel.
    for i := 0; i < numCPU; i++ {
        <-c    // wait for one task to complete
    }
    // All done.
}

// runtime will return numbers of CPU
runtime.NumCPU() // use this to replace numCPU=4

runtime.GOMAXPROCS(0) // query the value of user-specified cores number, defaults to runtime.NumCPU()
runtiime.GOMAXPROCS(4) // override runtime.NumCPU as 4

12.5 buffer 泄漏

// client
var freeList = make(chan *Buffer, 100)
var serverChan = make(chan *Buffer)

func client() {
    for {
        var b *Buffer
        // Grab a buffer if available; allocate if not.
        select {
        case b = <-freeList:
            // Got one; nothing more to do.
        default:
            // None free, so allocate a new one.
            b = new(Buffer)
        }
        load(b)              // Read next message from the net.
        serverChan <- b      // Send to server.
    }
}

// server
func server() {
    for {
        b := <-serverChan    // Wait for work.
        process(b)
        // Reuse buffer if there's room.
        select {
        case freeList <- b:
            // Buffer on free list; nothing more to do.
        default:
            // Free list full, just carry on.
        }
    }
}

13. Error

13.1 error

error built-in interface:

type error interface {
	Error() string
}

所以开发者可以实现自定义 Error 从而提供非常详细的错误信息,例如 os.PathError

// PathError records an error and the operation and
// file path that caused it.
type PathError struct {
    Op string    // "open", "unlink", etc.
    Path string  // The associated file.
    Err error    // Returned by the system call.
}

func (e *PathError) Error() string {
    return e.Op + " " + e.Path + ": " + e.Err.Error()
}

/* error info:
 * open /etc/passwx: no such file or directory
 */

// developer can be able handling the specific error
for try := 0; try < 2; try++ {
    file, err = os.Create(filename)
    if err == nil {
        return
    }
    // type assertion to handle the specific PathError
    if e, ok := err.(*os.PathError); ok && e.Err == syscall.ENOSPC {
        deleteTempFiles()  // Recover some space.
        continue
    }
    return
}

13.2 panic

当程序运行时遇到不可恢复的错误时,go 提供了一个内置函数 panic 用于创建一个运行时错误同时终止程序的运行。

var user = os.Getenv("USER")

func init() {
    if user == "" {
        panic("no value for $USER")
    }
}

13.3 recover

上一节中提到的 panic 被调用之后会立刻终止程序执行,并且开始释放 goroutine 栈并且执行 defer 函数直到栈顶之后程序就结束运行了。在这个过程中可以尝试使用内置的 recover 函数来重新获取 goroutine 的控制权从而恢复正常的运行。

// Error is the type of a parse error; it satisfies the error interface.
type Error string
func (e Error) Error() string {
    return string(e)
}

// error is a method of *Regexp that reports parsing errors by
// panicking with an Error.
func (regexp *Regexp) error(err string) {
    panic(Error(err))
}

// Compile returns a parsed representation of the regular expression.
func Compile(str string) (regexp *Regexp, err error) {
    regexp = new(Regexp)
    // doParse will panic if there is a parse error.
    defer func() {
        if e := recover(); e != nil {
            regexp = nil    // Clear return value.
            err = e.(Error) // Will re-panic if not a parse error.
        }
    }()
    return regexp.doParse(str), nil
}

References

  1. Effective Go - The Go Programming Language
  2. golang 要点速记

Public discussion