Getting Started with Go (Part 3) Coding Standards and Performance Optimization | Youth Training Camp

发表于 2022-05-12 20:44 1790 字 9 min read

cos avatar

cos

FE / ACG / 手工 / 深色模式强迫症 / INFP / 兴趣广泛养两只猫的老宅女 / remote

本文系统介绍了编写高质量 Go 代码的核心原则与实践方法,涵盖编码规范、注释与命名规范、控制流程优化、错误处理机制以及性能优化策略。通过使用 gofmt、goimports 等工具和清晰的命名、简洁的控制流程,提升代码的可读性与可维护性;在错误处理上强调使用 error 而非 panic,并合理使用 wrap、is、as 等操作;在性能方面推荐预分配内存、使用 strings.Builder 拼接字符串、利用空结构体节省内存等技巧,最终实现正确、可靠、简洁且高效的 Go 程序。

This article has been machine-translated from Chinese. The translation may contain inaccuracies or awkward phrasing. If in doubt, please refer to the original Chinese version.

This lesson covered how to write cleaner and clearer code. Each language has its own characteristics and unique coding standards. For Go, the available performance optimization techniques and handy tools were also introduced.

High-quality code needs to be correct, reliable, concise, and clear:

  • Correctness: Have all edge cases been considered? Can incorrect calls be handled?
  • Reliability: Are exception handling and error handling clear? Can dependent service anomalies be handled promptly?
  • Conciseness: Is the logic simple? Can new features be quickly supported?
  • Clarity and readability: Can other people clearly understand the code when reading it? Will refactoring not cause unpredictable situations? This requires coding standards.

Coding Standards

Formatting Tools

When it comes to coding standards, we must mention code formatting tools. It’s recommended to use the official Go formatting tool gofmt. GoLand has it built in, and common IDEs can easily configure it.

  • Another tool is goimports, which is equivalent to gofmt plus dependency package management, automatically adding and removing dependency packages.

image.png

In JS, there are similar formatting tools like Prettier, which can be used with ESLint for code formatting.

Comment Standards

Good comments should:

  • Explain what the code does

  • Explain complex or non-obvious logic

  • Explain why the code is implemented this way (these factors are hard to understand out of context)

  • Explain when the code might fail (explain some constraints)

  • Comment on public symbols (every public symbol declared in a package: variables, constants, functions, and structs)

    • Exception: no need to comment methods implementing interfaces

Google Style Guide has two rules:

  • Any public functionality that is neither obvious nor short must be commented.
  • Any function in a library must be commented regardless of length or complexity.

Situations to avoid:

  • Verbose comments on self-explanatory functions
  • Direct translation of obvious flows

In summary, code is the best comment:

  • Comments should provide contextual information not expressed in the code
  • Clean and clear code has no requirement for flow comments, but you can supplement with comments about why things are done a certain way, relevant background, etc., to provide useful information.

Naming Conventions

Variable Names

  • Brevity over verbosity

    • The scope of i vs index doesn’t need the extra verbosity of index
// Bad
for index := 0; index < len(s) ; index++ {
    // do something
}
// Good
for i := 0; i < len(s); i++ {
    // do something
}
  • Acronyms should be all caps, but when they appear at the beginning of a variable and don’t need to be exported, use all lowercase

    • Use ServeHTTP instead of ServeHttp
    • Use XMLHTTPRequest or xmlHTTPRequest
  • The farther a variable name is from where it’s used, the more context information it needs to carry.

    • For global variables, more context information is needed in the name so it can be easily identified in different places.
// Bad
func ( c *Client ) send( req *Request, t time.Time )

// Good
func ( c *Client ) send( req *Request, deadline time.Time )

Function Naming

  • Function names should not carry package name context information, because the package name and function name always appear together

    • For example, the function that creates a server in the http package: Serve > ServeHTTP, because it’s always called as http.Serve
  • Function names should be as short as possible

  • When a package named foo has a function that returns type T (where T is not Foo), you can include the return type information in the function name

    • When returning type Foo, it can be omitted without causing ambiguity

Package Names

  • Consist only of lowercase letters. No uppercase letters, underscores, or other characters.
  • Short and containing some context information. For example: schema, task, etc.
  • Don’t use the same name as standard libraries. For example, don’t use sync or strings. The following rules should be followed as much as possible, using standard library package names as examples:
  • Don’t use common variable names as package names. For example, use bufio instead of buf
  • Use singular instead of plural. For example, use encoding instead of encodings
  • Use abbreviations carefully. For example, fmt is shorter than format without breaking context

Overall, good naming reduces the cost of reading and understanding code, keeps attention on the main flow, and allows clear understanding of program functionality without frequently switching to branch details and having to explain them.

Control Flow

  • Avoid nesting, keep the normal flow clear and readable

    • Handle error/special cases first, return early or continue loops to reduce nesting
 // Bad
 if foo {
    return x
 } else {
    return nil
 }

 // Good
 if foo {
    return x
 }
 return nil
  • Keep the normal code path at minimum indentation, reduce nesting
 // Bad
 func OneFunc() error {
    err := doSomething()
    if err == nil {
       err := doAnotherThing()
       if err == nil {
          return nil // normal case
       }
       return err
    }
    return err
 }

 // Good
 func OneFunc() error {
    if err := doSomething(); err != nil {
       return err
    }
    if err := doSomething(); err != nil {
       return err
    }
    return nil // normal case
 }

In summary, the flow handling logic in programs should go in a straight line as much as possible, avoiding complex nested branches, letting the normal flow code move downward along the screen. This improves code maintainability and readability, because most failure issues appear in complex conditional and loop statements.

Error Handling

  • Simple errors

    • Simple errors refer to errors that occur only once and don’t need to be caught elsewhere
    • Prefer using errors.New to create anonymous variables to directly represent simple errors
    • If formatting is needed, use fmt.Errorf
 func defaultCheckRedirect(req *Request, via []*Request) error {
    if len(via) >= 10 {
       return errors.New("stopped after 10 redirects")
    }
    return nil
 }
  • Complex errors: Use error Wrap and Unwrap

    • Error Wrap essentially provides the ability to nest one error inside another error, generating an error trace chain
    • Use the %w keyword in fmt.Errorf to associate an error with the error chain
    • Use errors.Is to determine if an error is a specific error, checking all errors in the error chain (go/wrap_test.go · golang/go)
    • Use errors.As to get a specific type of error from the error chain and assign it to a defined variable (go/wrap_test.go · golang/go)

In Go, something more severe than an error is panic, which indicates that the program cannot work normally.

  • Using panic in business code is not recommended

    • After a panic occurs, it propagates up to the top of the call stack
    • If none of the calling functions contain recover, the entire program will crash
    • If the problem can be masked or resolved, it’s recommended to use error instead of panic
  • When an irreversible error occurs during program startup, panic can be used in the init or main function (sarama/main.go · Shopify/sarama)

Where there’s panic, recover naturally comes up. If a third-party library’s bug causes a panic that affects your own logic, you need recover.

  • recover can only be used in deferred functions; nesting won’t work, and it only takes effect in the current goroutine (github.com/golang/go/b…)
  • defer statements are last-in-first-out
  • If more context information is needed, you can log the current call stack after recover (github.com/golang/webs…)

Summary

  • error should provide concise contextual information chains to facilitate problem identification
  • panic is for truly exceptional situations
  • recover takes effect within the current goroutine’s deferred functions

Performance Optimization Suggestions

  • Prerequisite: Improve program efficiency as much as possible while meeting quality factors like correctness, reliability, conciseness, and clarity.
  • Trade-offs: Sometimes time efficiency and space efficiency can be opposing; appropriate trade-offs need to be made based on importance analysis.

Based on Go language characteristics, the lesson introduced many Go-related performance optimization suggestions:

Pre-allocate Memory

When initializing slices with make(), provide capacity information whenever possible.

 func PreAlloc(size int) {
    data := make([]int, 0, size)
    for k := 0; k < size; k++ {
       data = append(data, k)
    }
 }

This is because a slice is essentially a description of an array segment, including the array pointer, segment length, and segment capacity (maximum length without changing memory allocation).

  • Slice operations don’t copy the elements pointed to by the slice
  • Creating a new slice reuses the underlying array of the original slice. So pre-setting the capacity value can avoid extra memory allocations and achieve better performance.

String Processing Optimization

Using strings.Builder for common string concatenation:

  • + concatenation (slowest)

  • strings.Builder (fastest)

  • bytes.Buffer Principle: Strings in Go are immutable types with fixed memory size

  • When concatenating with +, a new string is generated in a new space, with the new space being the sum of the two original strings

  • strings.Builder and bytes.Buffer allocate memory in multiples

  • Both strings.Builder and bytes.Buffer use []byte arrays underneath

    • bytes.Buffer allocates a new space for the generated string variable when converting to string
    • strings.Builder directly converts the underlying []byte to string type and returns it
 func PreStrBuilder(n int, str string) string {
    var builder strings.Builder
    builder.Grow(n * len(str))
    for i := 0; i < n; i++ {
       builder.WriteString(str)
    }
    return builder.String()
 }

Empty Struct

  • An empty struct instance doesn’t occupy any memory space

  • Can be used as a placeholder in various scenarios

    • Saves memory
    • The empty struct itself has strong semantics, meaning no value is needed here, serving only as a placeholder
  • For example, when implementing a Set, use the map’s key while setting the value to an empty struct (golang-set/threadunsafe…)

Related Links

Summary and Reflections

This lesson introduced common coding standards in Go and other languages, and proposed Go-specific performance optimization suggestions. A performance optimization hands-on exercise using the pprof tool followed.

Notes content from Teacher Zhang Lei’s course in the 3rd Youth Training Camp: “High-Quality Programming and Performance Tuning Practice”
Course materials: Go Language Principles and Practice Learning Materials (Part 1) - 3rd ByteDance Youth Training Camp - Backend Track

喜欢的话,留下你的评论吧~

© 2020 - 2026 cos @cosine
Powered by theme astro-koharu · Inspired by Shoka