Skip to content

Effective Go tips and notes

homepage-banner

“The original intention of the document ‘Effective Go’ is to emphasize the importance of understanding the features and idioms of Go in order to write good Go code. It also highlights the significance of grasping the established conventions of Go programming, including naming, formatting, and program structure, to ensure that the written code is easily understood by other Go programmers.” However, “Effective Go” was written and published in 2009 and has not been significantly updated since then. While it serves as a helpful guide for understanding the language itself, it rarely mentions libraries and does not cover major changes in the Go ecosystem that have occurred since its writing, such as build systems, testing, modules, and polymorphism. Therefore, “Effective Go” remains useful, but it is important to note that it is not a comprehensive guide.

1. Code Formatting

Use the gofmt tool to standardize the formatting of Go code.

Here are some formatting conventions for Go code:

  • Indentation: Go uses tabs for indentation, unless spaces are necessary.
  • Line length: Go does not have a line length limit. If a line is too long for readability, you can use tabs to indent and wrap it.
  • Parentheses: Avoid using parentheses for constructs like for, if, switch, etc. Additionally, control the precedence of operators using spaces, such as x << 8 + y << 16.

2. Code Comments

  • Block comments: /* */
  • Line comments: //
  • Comments before any code without line breaks are considered as self-declarations of the document, for example, /* Copyright 2023 Alibaba.Inc */

3. Naming

3.1 Package Naming

Usually, package names are written in lowercase, with single words, and do not use underscores or mixed case. Due to the existence of package names, there are some points to consider when naming exported members:

  • For example, if a package exports a type called “reader”, it can be named as “Reader” to be clear and concise, without the need for “BufReader”. This way, when referencing it, it can be expressed as “bufio.Reader”, which is clear. Adding “bufio.BufReader” would be redundant.
  • For example, when naming package-exported constructors, let’s take the constructors in the “ring” package as an example: “ring.NewRing()”, “ring.Ring()”, “ring.New()”. The “New” naming convention is clear and concise.

3.2 Naming of getters and setters

In Go, there are no getter and setter decorators. Taking obj.Owner as an example, it is usually named as SetOwner and GetOwner. However, following the principle of clear and concise naming, it can be named as obj.Owner(), obj.SetOwner().

3.3 Naming of interfaces

  • For interfaces with only one method, the naming convention can be method name + "er", for example, Reader.
  • Use standardized and meaningful names, such as Read, Write, Close, Flush, String, etc.

4. Semicolons

In Go, unlike C, semicolons are not required as separators. This leads to the issue that control structures like if, for, switch, and select cannot have their parentheses on a new line:

if i < f() {

}

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

}

5. Control Structures

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. Functions

6.1 Multiple Return Values

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 Return Value Naming

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.
}

7. Data

7.1 new

new allocates memory but does not initialize it. new(T) allocates storage space for a variable of type T with all zeros and returns its address. The return type is *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 is used to initialize certain types of features, including: map, slice, and channel. The memory initialized by make is not all set to 0, and the returned type is 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 is a fixed-length slice. There are several differences between go arrays and c arrays:

  • 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

Sliced wrapped arrays provide a more versatile, powerful, and convenient interface for data sequences. In Go, most array programming is done using slices instead of simple arrays, except for items with explicit dimensions, such as transformation matrices.

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 function prototype:

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

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

8. Initialization

8.1 Constant

In Go, iota is used for automatic initialization and creation of enumerations:

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 Function

Each source code file can define an init function, which is a niladic init function (no arguments). Another key point is the timing of the init function execution: Init is called after the evaluation of all variable declarations in the package and only after all imported packages have been initialized.

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. Methods

9.1 Pointers and Values

Go defaults to pass by value, except for pointers and interfaces.

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 in Go is used to specify the behavior of an 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 Conversion

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

The blank identifier can be of any type and have any value.

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 Parallel

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 Leak

// 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
}

So developers can implement custom Errors to provide very detailed error information, for example 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

When encountering an unrecoverable error during program execution, Go provides a built-in function panic to create a runtime error and terminate the program’s execution.

var user = os.Getenv("USER")

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

13.3 recover

In the previous section, it was mentioned that when panic is called, it immediately terminates the program execution, releases the goroutine stack, and executes the defer functions until the top of the stack. After that, the program ends. During this process, you can try using the built-in recover function to regain control of the goroutine and restore normal operation.

// 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

  • https://www.edony.ink/key-points-of-effective-go/
  • https://go.dev/doc/effective_go
Leave a message