Golang Coding Standard Basic Guidelines
Use defer to release resources
Use defer to release resources such as files and locks.
Not recommended:
p.Lock()
if p.count < 10 {
p.Unlock()
return p.count
}
p.count++
newCount := p.count
p.Unlock()
return newCount
// It's easy to forget to unlock when there are multiple return branches
Recommended:
p.Lock()
defer p.Unlock()
if p.count < 10 {
return p.count
}
p.count++
return p.count
// More readable
The overhead of defer is extremely low, so it should only be avoided when you can prove that the function execution time is in the nanosecond level. Using defer to improve readability is worthwhile because the cost of using them is negligible. This is especially applicable to larger methods that involve more than just simple memory access, where the resource consumption of other calculations far exceeds that of defer
.
Channel size should be 1 or unbuffered
Channels should typically have a size of 1 or be unbuffered. By default, channels are unbuffered with a size of zero. Any other size must be strictly reviewed. We need to consider how to determine the size, consider what prevents the channel from writing under high loads and when blocked, and consider what changes occur in the system logic when this happens.
Not recommended:
// Should be enough to handle any situation!
c := make(chan int, 64)
Recommended:
// Size: 1
c := make(chan int, 1) // or
// Unbuffered channel, size is 0
c := make(chan int)
Enums start from 1
The standard method of introducing enums in Go is to declare a custom type and a const group that uses iota. Since the default value of variables is 0, enums should typically start with a non-zero value.
Not recommended:
type Operation int
const (
Add Operation = iota
Subtract
Multiply
)
// Add=0, Subtract=1, Multiply=2
Recommended:
type Operation int
const (
Add Operation = iota + 1
Subtract
Multiply
)
// Add=1, Subtract=2, Multiply=3
In some cases, using the zero value makes sense (enums starting from zero), for example, when the zero value is the ideal default behavior.
type LogOutput int
const (
LogToStdout LogOutput = iota
LogToFile
LogToRemote
)
// LogToStdout=0, LogToFile=1, LogToRemote=2
Using atomic
Use atomic operations from the sync/atomic package to operate on primitive types (int32
, int64
, etc.) because it is easy to forget to use atomic operations to read from or modify variables.
go.uber.org/atomic adds type safety to these operations by hiding the underlying type. Additionally, it includes a convenient atomic.Bool
type.
Not recommended approach:
type foo struct {
running int32 // atomic
}
func (f* foo) start() {
if atomic.SwapInt32(&f.running, 1) == 1 {
// already running…
return
}
// start the Foo
}
func (f *foo) isRunning() bool {
return f.running == 1 // race!
}
Recommended approach:
type foo struct {
running atomic.Bool
}
func (f *foo) start() {
if f.running.Swap(true) {
// already running…
return
}
// start the Foo
}
func (f *foo) isRunning() bool {
return f.running.Load()
}
Avoid mutable global variables
Use the dependency injection approach to avoid changing global variables. This is applicable for function pointers as well as other value types.
Not recommended approach 1:
// sign.go
var _timeNow = time.Now
func sign(msg string) string {
now := _timeNow()
return signWithTime(msg, now)
}
Recommended approach 1:
// sign.go
type signer struct {
now func() time.Time
}
func newSigner() *signer {
return &signer{
now: time.Now,
}
}
func (s *signer) Sign(msg string) string {
now := s.now()
return signWithTime(msg, now)
}
Not recommended approach 2:
// sign_test.go
func TestSign(t *testing.T) {
oldTimeNow := _timeNow
_timeNow = func() time.Time {
return someFixedTime
}
defer func() { _timeNow = oldTimeNow }()
assert.Equal(t, want, sign(give))
}
Recommended approach 2:
// sign_test.go
func TestSigner(t *testing.T) {
s := newSigner()
s.now = func() time.Time {
return someFixedTime
}
assert.Equal(t, want, s.Sign(give))
}
Avoid Using Pre-declared Identifiers
The Go Language Specification outlines several predeclared identifiers that should not be used in Go projects. These predeclared identifiers should not be reused as names in different contexts, as doing so will hide the original identifiers in the current scope (or any nested scope), potentially leading to code confusion. In the best case, the compiler will raise an error; in the worst case, such code may introduce potential, difficult-to-recover errors.
Unrecommended Practice 1:
var error string
// `error` implicitly shadows the built-in identifier
// or
func handleErrorMessage(error string) {
// `error` implicitly shadows the built-in identifier
}
Recommended Practice 1:
var errorMessage string
// `error` now points to the non-shadowed built-in identifier
// or
func handleErrorMessage(msg string) {
// `error` now points to the non-shadowed built-in identifier
}
Unrecommended Practice 2:
type Foo struct {
// While these fields technically do not shadow, redefining `error` or `string` strings now becomes ambiguous.
error error
string string
}
func (f Foo) Error() error {
// `error` and `f.error` visually appear similar
return f.error
}
func (f Foo) String() string {
// `string` and `f.string` visually appear similar
return f.string
}
Recommended Practice 2:
type Foo struct {
// `error` and `string` are now explicit.
err error
str string
}
func (f Foo) Error() error {
return f.err
}
func (f Foo) String() string {
return f.str
}
Note that the compiler will not generate errors when using the predeclared identifiers, but tools like go vet
will correctly point out these and other implicitly-related issues.
Avoid using init()
Try to avoid using init()
as much as possible. When init()
is unavoidable or preferred, the code should try to:
- Ensure completeness regardless of the program environment or call.
- Avoid relying on the order or side effects of other
init()
functions. Although the order ofinit()
is explicit, the code can change, so the relationship betweeninit()
functions may make the code fragile and error-prone. - Avoid accessing or manipulating global or environmental states, such as machine information, environment variables, working directories, program parameters/inputs, etc.
- Avoid I/O, including file systems, networking, and system calls.
Code that does not meet these requirements may belong as part of the main()
call (or elsewhere in the program’s lifecycle) or be written as part of main()
itself. In particular, libraries intended to be used by other programs should pay special attention to completeness rather than performing “init magic”.
Unrecommended approach 1:
type Foo struct {
// ...
}
var _defaultFoo Foo
func init() {
_defaultFoo = Foo{
// ...
}
}
Recommended approach 1:
var _defaultFoo = Foo{
// ...
}
// or, for better testability:
var _defaultFoo = defaultFoo()
func defaultFoo() Foo {
return Foo{
// ...
}
}
Unrecommended approach 2:
type Config struct {
// ...
}
var _config Config
func init() {
// Bad: based on the current directory
cwd, _ := os.Getwd()
// Bad: I/O
raw, _ := os.ReadFile(
path.Join(cwd, "config", "config.yaml"),
)
yaml.Unmarshal(raw, &_config)
}
Recommended approach 2:
type Config struct {
// ...
}
func loadConfig() Config {
cwd, err := os.Getwd()
// handle error
raw, err := os.ReadFile(
path.Join(cwd, "config", "config.yaml"),
)
// handle error
var config Config
yaml.Unmarshal(raw, &config)
return config
}
Given the above considerations, in some cases, init()
may be more preferred or necessary, including:
- Cannot be represented as a single assignment of a complex expression.
- Insertable hooks, such as
database/sql
, type registries, etc.
Prefer specifying slice capacity when appending
Always prioritize specifying a capacity value for make()
when initializing a slice to be appended.
Unrecommended approach:
for n := 0; n < b.N; n++ {
data := make([]int, 0)
for k := 0; k < size; k++{
data = append(data, k)
}
}
Recommended approach:
for n := 0; n < b.N; n++ {
data := make([]int, 0, size)
for k := 0; k < size; k++{
data = append(data, k)
}
}
Using Field Tags in Struct Serialization
When serializing to JSON, YAML, or any other format that supports field naming based on tags, relevant tags should be used for annotation.
Not Recommended:
type Stock struct {
Price int
Name string
}
bytes, err := json.Marshal(Stock{
Price: 137,
Name: "UBER",
})
Recommended:
type Stock struct {
Price int `json:"price"`
Name string `json:"name"`
// Safe to rename Name to Symbol.
}
bytes, err := json.Marshal(Stock{
Price: 137,
Name: "UBER",
})
In theory, the serialization format of a structure is a contract between different systems. Making changes to the serialization form of the structure (including field names) will break this contract. Specifying field names in tags makes the contract explicit and also helps prevent accidental violations of the contract through refactoring or renaming of fields.