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 likegofmtplus dependency management, automatically adding and removing dependency packages.
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
iandindexdoes not require the additional verbosity ofindex.
- The scope of
// 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
ServeHTTPinstead ofServeHttp. - Use
XMLHTTPRequestorxmlHTTPRequest.
- For example, use
-
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 ashttp.Serve.
- For example, in the HTTP package, the function for creating a service is
-
Function names should be as short as possible.
-
When a function in a package named
fooreturns a typeT(whereTis notFoo), the return type information can be included in the function name.- When returning
Footype, it can be omitted without causing ambiguity.
- When returning
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
syncorstrings. 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
bufioinstead ofbuf. - Use singular rather than plural. For example, use
encodinginstead ofencodings. - Use abbreviations cautiously. For example, using
fmtis shorter thanformatwithout 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.Newto 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
WrapandUnwrap.- Error
Wrapactually provides the ability to nest oneerrorwithin another, creating a chain oferrortracking. - Use the
%wkeyword infmt.Errorfto associate an error with the error chain. - Use
errors.Isto determine if an error is a specific error, which can check all errors in the chain (go/wrap_test.go · golang/go). - Use
errors.Asto retrieve a specific type of error from the error chain and assign it to a defined variable. (go/wrap_test.go · golang/go).
- Error
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
panicoccurs, 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
errorinstead ofpanic.
- When a
-
When an irreversible error occurs during the program's startup phase,
paniccan be used in theinitormainfunction. (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.
recovercan only be used in functions that aredeferd; 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#
errorshould provide as concise a contextual information chain as possible to facilitate problem location.panicis used for truly exceptional situations.recovertakes effect in the current goroutine within the function that isdeferd.
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.Bufferprinciple: 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.Builderandbytes.Bufferallocate memory in multiples. -
Both
strings.Builderandbytes.Bufferare based on[]bytearrays.bytes.Bufferallocates a new space to store the generated string variable when converting to a string.strings.Builderdirectly converts the underlying[]byteto 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
structdoes 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...)
Related Links#
- “golang pprof Practical” code experiment cases: github.com/wolfogre/go…
- Try using the test command to write and run simple tests go.dev/doc/tutoria…
- Try using the -bench parameter to perform performance testing on the written functions, pkg.go.dev/testing#hdr…
- Go Code Review Suggestions github.com/golang/go/w…
- Uber's Go Coding Standards, github.com/uber-go/gui…
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