banner
cos

cos

愿热情永存,愿热爱不灭,愿生活无憾
github
tg_channel
bilibili

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

This lesson discusses how to write cleaner and clearer code. Every language has its own characteristics and unique coding standards. For Go, various performance optimization techniques and handy tools are also introduced.

High-quality code should possess characteristics of correctness, reliability, simplicity, and clarity.

  • Correctness: Are all boundary conditions fully considered? Can erroneous calls be handled?
  • Reliability: Are exceptions or error handling clear? Can dependent service exceptions be handled promptly?
  • Simplicity: Is the logic simple? Can new features be quickly supported in the future?
  • Clarity: Can others clearly understand the code when reading it? Will there be concerns about unforeseen situations during refactoring?
    This requires coding standards.

Coding Standards#

Formatting Tools#

When mentioning coding standards, we must talk about code formatting tools. It is recommended to use the official Go formatting tool gofmt, which is built into Goland and can be easily configured in common IDEs.

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

image.png

There is also a similar formatting tool in JavaScript called Prettier, which can be used in conjunction with ESLint for code formatting.

Commenting Standards#

Good comments need to:

  • Explain the purpose of the code.

  • Explain complex or non-obvious logic.

  • Explain the reasons behind the code implementation (these factors can be hard to understand when taken out of context).

  • Explain under what circumstances the code might fail (explain some constraints).

  • Explain comments for public symbols (every public symbol declared in a package: variables, constants, functions, and structures, etc.).

    • Exception: No need to comment on methods that implement interfaces.

The Google Style Guide has two rules:

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

Situations to avoid include:

  • Verbose comments on visible names that are self-explanatory.
  • Direct translations of obvious processes.

In summary, code is the best comment.

  • Comments should provide contextual information not expressed by the code.
  • Concise and clear code does not require process comments, but comments can supplement information about why something is done, the relevant background of the code, etc.

Naming Standards#

Variable Names#

  • Conciseness is preferred over verbosity.

    • The scope of i and index does not require the additional 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 fully capitalized, but when they appear at the beginning of a variable and do not need to be exported, use all lowercase.

    • For example, use ServeHTTP instead of ServeHttp.
    • Use XMLHTTPRequest or xmlHTTPRequest.
  • The further a variable name is from where it is used, the more contextual information it should carry.

    • For example, global variables need more contextual information in their names to be easily recognized 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 do not carry contextual information from the package, as package names and function names always appear together.

    • For example, in the HTTP package, the function for creating a service is Serve > ServeHTTP, because it is always called as http.Serve.
  • Function names should be as short as possible.

  • When a function in a package named foo returns a type T (where T is not Foo), the return type information can be included in the function name.

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

Package Names#

  • Should consist only of lowercase letters. Do not include uppercase letters or underscores.
  • Should be short and contain some contextual information, such as schema, task, etc.
  • Should not have the same name as standard libraries. For example, do not use sync or strings. The following rules should be followed as much as possible, using standard library package names as examples:
  • Do not use common variable names as package names. For example, use bufio instead of buf.
  • Use singular rather than plural. For example, use encoding instead of encodings.
  • Use abbreviations cautiously. For example, using fmt is shorter than format without losing context.

In general, good naming reduces the cost of reading and understanding code, allowing people to focus on the main flow and clearly understand the program's functionality, rather than frequently switching to branch details that must be explained.

Control Flow#

  • Avoid nesting and keep the normal flow clear and readable.

    • Prioritize handling error cases/special cases, returning early or continuing loops to reduce nesting.
 // Bad
 if foo {
    return x
 } else {
    return nil
 }
 ​
 // Good
 if foo {
    return x
 }
 return nil
  • Try to keep the normal code path with minimal indentation to 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 logic for handling flow in a program should aim to move in a straight line, avoiding complex nested branches, allowing normal flow code to move down the screen. This enhances code maintainability and readability, as faults often occur in complex conditional statements and loops.

Error Handling#

  • Simple Errors

    • Simple errors refer to errors that occur only once and do not 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 actually provides the ability to nest one error within another, creating a chain of error tracking.
    • 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, which can check all errors in the chain (go/wrap_test.go · golang/go).
    • Use errors.As to retrieve a specific type of error from the error chain and assign it to a defined variable. (go/wrap_test.go · golang/go).

In Go, more serious than errors is panic, which indicates that the program cannot function normally.

  • It is not recommended to use panic in business code.

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

With panic, naturally, we mention recover. If a panic is caused by a bug in another library that affects its own logic, then recover is needed.

  • recover can only be used in functions that are deferd; it does not work in nested calls and only takes effect in the current goroutine (github.com/golang/go/b…).
  • The statements in defer are last in, first out.
  • If more contextual information is needed, you can log the current call stack after recovering (github.com/golang/webs…).

Summary#

  • error should provide as concise a contextual information chain as possible to facilitate problem location.
  • panic is used for truly exceptional situations.
  • recover takes effect in the current goroutine within the function that is deferd.

Performance Optimization Suggestions#

  • Prerequisite: Improve program efficiency as much as possible while meeting quality factors such as correctness, reliability, simplicity, and clarity.
  • Trade-offs: Sometimes time efficiency and space efficiency may be at odds, requiring analysis of importance for appropriate trade-offs.

Based on the characteristics of the Go language, many performance optimization suggestions related to Go were introduced in class:

Pre-allocating Memory#

When using make() to initialize slices, 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 a segment of an array, including the array pointer, the length of the segment, and the capacity of the segment (the maximum length without changing memory allocation).

  • Slice operations do not 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 can avoid additional memory allocations and achieve better performance.

String Processing Optimization#

Use strings.Builder for common string concatenation methods.

  • + for concatenation (the slowest).

  • strings.Builder (the fastest).

  • bytes.Buffer principle: Strings in Go are immutable types, and their memory size is fixed.

  • Using + for concatenation generates a new string, allocating a new space that is the sum of the sizes of the original two strings.

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

  • Both strings.Builder and bytes.Buffer are based on []byte arrays.

    • bytes.Buffer allocates a new space to store the generated string variable when converting to a string.
    • strings.Builder directly converts the underlying []byte to a string type for return.
 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 Structs#

  • An instance of an empty struct struct does not occupy any memory space.

  • Can be used as placeholders in various scenarios.

    • Saves memory space.
    • An empty struct itself has strong semantics, indicating that no value is needed here, only serving as a placeholder.
  • For example, when implementing a Set, use the keys of a map and set the values to an empty struct. (golang-set/threadunsafe...)

Summary and Insights#

This lesson introduced common coding standards in Go and other languages, and provided performance optimization suggestions related to the Go language. Practical exercises on performance optimization were also conducted using the pprof tool.

The notes are based on the course "High-Quality Programming and Performance Optimization Practice" by Teacher Zhang Lei from the third Training Camp.
Course materials: 【Go Language Principles and Practice Learning Materials (Part 1)】Third ByteDance Training Camp - Back-end Special

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.