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 likegofmt
plus 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
i
andindex
does 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
ServeHTTP
instead ofServeHttp
. - Use
XMLHTTPRequest
orxmlHTTPRequest
.
- 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
foo
returns a typeT
(whereT
is notFoo
), the return type information can be included in the function name.- When returning
Foo
type, 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
sync
orstrings
. 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 ofbuf
. - Use singular rather than plural. For example, use
encoding
instead ofencodings
. - Use abbreviations cautiously. For example, using
fmt
is shorter thanformat
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
andUnwrap
.- Error
Wrap
actually provides the ability to nest oneerror
within another, creating a chain oferror
tracking. - Use the
%w
keyword infmt.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).
- 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
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 ofpanic
.
- When a
-
When an irreversible error occurs during the program's startup phase,
panic
can be used in theinit
ormain
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 aredefer
d; 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 isdefer
d.
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
andbytes.Buffer
allocate memory in multiples. -
Both
strings.Builder
andbytes.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...)
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