Special Delivery a Clear Go Coding Standard Ready to Apply

Special Delivery A Clear Go Coding Standard Ready to Apply #

Hello, I’m Kong Lingfei.

In our previous lesson, we discussed the “Methodology for Writing Elegant Go Projects”. The content of that lesson was very rich and it was a culmination of my years of experience in Go project development. I encourage you to take some extra time to digest and absorb the content thoroughly. After having a feast, in today’s special episode, I will provide you with a coding standard that I mentioned in the previous lesson. In this lesson, in order to save you time and effort, I will give you a clear and directly applicable Go coding standard to help you write high-quality Go applications.

This standard is based on the Go language’s official coding standard as well as some reasonable standards accumulated by the Go community. After that, I added my own understanding and summarized it. It is more comprehensive than many internal company standards. Once you have mastered it, you will be highly regarded during interviews at top companies or when writing code in those companies, as it will make others think that your code is very professional.

This coding standard includes nine categories: code style, naming conventions, comment conventions, types, control structures, functions, GOPATH setting conventions, dependency management, and best practices. If you feel that there is too much content in these standards and you can’t remember them all after reading them once, don’t worry. You can read them multiple times or refer back to them when you need to apply them in practice. The content of this special episode serves more as a reference manual for when you are writing code.

1. Code Style #

1.1 Code Formatting #

  • Code must be formatted using gofmt.

  • There should be spaces between operators and operands.

  • It is recommended to keep each line of code within 120 characters. If it exceeds the limit, appropriate line breaks should be used. However, there are exceptions such as import statements, automatically generated code, and struct fields with tags.

  • File length should not exceed 800 lines.

  • Function length should not exceed 80 lines.

  • Import guidelines:

    • Code must be formatted using goimports (it is recommended to set up your Go code editor to automatically run goimports on save).
    • Avoid using relative package imports, e.g., import …/util/net.
    • When the package name does not match the last directory name in the import path or when there are conflicting package names, aliases must be used for imports.
// bad
"github.com/dgrijalva/jwt-go/v4"

// good
jwt "github.com/dgrijalva/jwt-go/v4"
  • Packages should be grouped together. Anonymous packages should be referenced in a separate group, with explanations provided for the use of anonymous packages.
import (
  // Go standard packages
  "fmt"

  // Third-party packages
  "github.com/jinzhu/gorm"
  "github.com/spf13/cobra"
  "github.com/spf13/viper"

  // Separate group for anonymous packages, with explanations
  // import mysql driver
  _ "github.com/jinzhu/gorm/dialects/mysql"

  // Internal packages
  v1 "github.com/marmotedu/api/apiserver/v1"
  metav1 "github.com/marmotedu/apimachinery/pkg/meta/v1"
  "github.com/marmotedu/iam/pkg/cli/genericclioptions"
)

1.2 Declarations, Initialization, and Definitions #

  • When multiple variables need to be used within a function, they can be declared using var at the beginning of the function. Outside of functions, var must be used, and := should be avoided to prevent scoping issues.
var (
  Width  int
  Height int
)
  • When initializing a struct reference, use &T{} instead of new(T) to make it consistent with struct initialization.
// bad
sptr := new(T)
sptr.Name = "bar"

// good
sptr := &T{Name: "bar"}
  • Struct declarations and initializations should be done on separate lines, as shown below.
type User struct{
    Username  string
    Email     string
}

user := User{
  Username: "colin",
  Email: "[[email protected]](/cdn-cgi/l/email-protection)",
}
  • Similar declarations should be grouped together, which also applies to constant, variable, and type declarations.
// bad
import "a"
import "b"

// good
import (
  "a"
  "b"
)
  • Specify container capacity whenever possible to pre-allocate memory for the container, for example:
v := make(map[int]string, 4)
v := make([]string, 0, 4)
  • At the top level, use the standard var keyword. Do not specify the type unless it differs from the type of the expression.
// bad
var _s string = F()

func F() string { return "A" }

// good
var _s = F()
// Since F already explicitly returns a string, we don't need to explicitly specify the type of _s
// It will still be of type string

func F() string { return "A" }
  • For unexported top-level constants and variables, use _ as a prefix.
// bad
const (
  defaultHost = "127.0.0.1"
  defaultPort = 8080
)

// good
const (
  _defaultHost = "127.0.0.1"
  _defaultPort = 8080
)
  • Embedded types (e.g., mutex) should be placed at the top of the field list in a struct and separated from regular fields with an empty line.
// bad
type Client struct {
  version int
  http.Client
}

// good
type Client struct {
  http.Client
}
  version int
}

1.3 Error Handling #

  • When an error is returned as a function value, it must be handled or the return value should be explicitly ignored using _. For defer xx.Close(), it doesn’t need explicit handling.

    func load() error { // normal code }

    // bad load()

    // good _ = load()

  • When an error is returned as a function value and there are multiple return values, the error must be the last parameter.

    // bad func load() (error, int) { // normal code }

    // good func load() (int, error) { // normal code }

  • Handle errors as early as possible and return early to reduce nesting.

    // bad if err != nil { // error code } else { // normal code }

    // good if err != nil { // error handling return err } // normal code

  • If the result of a function call needs to be used outside of an if statement, follow this pattern.

    // bad if v, err := foo(); err != nil { // error handling }

    // good v, err := foo() if err != nil { // error handling }

  • Handle errors separately, don’t combine them with other logic.

    // bad v, err := foo() if err != nil || v == nil { // error handling return err }

    // good v, err := foo() if err != nil { // error handling return err }

    if v == nil { // error handling return errors.New(“invalid value v”) }

  • If initialization is required for returned values, use this pattern.

    v, err := f() if err != nil { // error handling return // or continue. } // use v

  • Recommendations for error description:

    • Tell users what they can do instead of telling them what they cannot do.

    • Use “must” instead of “should” when expressing a requirement. For example, “must be greater than 0” or “must match regex ‘[a-z]+’”.

    • Use “must not” when expressing an incorrect format. For example, “must not contain”.

    • Use “may not” when expressing an action. For example, “may not be specified when otherField is empty” or “only name may be specified”.

    • Use single quotes to indicate a literal string value. For example, “must not contain ‘…’”.

    • Use backticks when referring to another field name. For example, “must be greater than request”.

    • Use words instead of symbols when specifying inequality. For example, “must be less than 256”, “must be greater than or equal to 0” (instead of “larger than”, “bigger than”, “more than”, “higher than”).

    • Use inclusive ranges whenever possible when specifying a range of numbers.

    • For Go 1.13 and above, it is recommended to use fmt.Errorf("module xxx: %w", err) for error generation.

    • Error descriptions should start with a lowercase letter and should not end with punctuation marks. For example:

      // bad errors.New(“Redis connection failed”) errors.New(“redis connection failed.”)

      // good errors.New(“redis connection failed”)

1.4 Handling Panics #

  • Avoid using panic in business logic.
  • In the main package, panic should only be used when the program cannot run at all, such as when it cannot open a file or connect to a database.
  • In the main package, use log.Fatal to log errors. This way the program can be terminated by the log, or the panic exception can be logged to a file for easy troubleshooting.
  • Exported interfaces must not have panics.
  • Within a package, it is recommended to use error instead of panic to pass errors.

1.5 Unit Testing #

  • Unit test file names should follow the naming convention example_test.go.
  • Test cases should be written for every important exported function.
  • Since functions within a unit test file are not exposed externally, exported structs and functions can be documented without comments.
  • If there is a function func (b *Bar) Foo, the unit test function can be func TestBar_Foo.

1.6 Handling Type Assertion Failures #

A type assertion with a single return value for an incorrect type will cause a panic. Always use the idiomatic “comma, ok” pattern.

// bad
t := n.(int)

// good
t, ok := n.(int)
if !ok {
	// error handling
}
// normal code

2. Naming Conventions #

Naming conventions are a very important part of code conventions. A unified, concise, and precise naming convention can greatly improve code readability and help avoid unnecessary bugs.

2.1 Package Naming #

  • Package names must match the directory names and should be meaningful and short. They should not conflict with the standard library.
  • Package names should be all lowercase with no uppercase letters or underscores. Use multiple directories to indicate hierarchy.
  • Project names can use hyphens to connect multiple words.
  • Package names and directory names should not be plural. For example, it should be net/url instead of net/urls.
  • Avoid using broad and meaningless package names like common, util, shared, or lib.
  • Package names should be simple and clear, such as net, time, log.

2.2 Function Naming #

  • Function names should use camel case, with the first letter capitalized or lowercase depending on the access control. For example: MixedCaps or mixedCaps.
  • Code-generated files (e.g., xxxx.pb.go) and test case groups using underscores (e.g., TestMyFunction_WhatIsBeingTested) are exceptions to this rule.

2.3 File Naming #

  • File names should be short and meaningful.
  • File names should be lowercase and use underscores to separate words.

2.4 Struct Naming #

  • Struct names should use camel case, with the first letter capitalized or lowercase depending on the access control. For example: MixedCaps or mixedCaps.
  • Struct names should not be verbs, they should be nouns, such as Node, NodeSpec.
  • Avoid using meaningless struct names like Data, Info.
  • Struct declarations and initializations should be done on separate lines. For example:
// User multi-line declaration
type User struct {
    Name  string
    Email string
}

// Multi-line initialization
u := User{
    UserName: "colin",
    Email:    "colin@example.com",
}

2.5 Interface Naming #

  • Interface naming follows the same rules as struct naming:

    • If the interface consists of a single function, the name should end with “er” (e.g., Reader, Writer). Sometimes this may result in poor English, but that’s okay.
    • If the interface consists of two functions, the name should be a combination of the two function names (e.g., ReadWriter).
    • If the interface consists of three or more functions, the name should be similar to a struct name.

For example:

// Seeking to an offset before the start of the file is an error.
// Seeking to any positive offset is legal, but the behavior of subsequent
// I/O operations on the underlying object is implementation-dependent.
type Seeker interface {
    Seek(offset int64, whence int) (int64, error)
}

// ReadWriter is the interface that groups the basic Read and Write methods.
type ReadWriter interface {
    Reader
    Writer
}

2.6 Variable Naming #

  • Variable names must follow camel case, with the first letter capitalized or lowercase depending on the access control.

  • In relatively simple environments (with few objects and strong targeting), some names can be abbreviated to a single letter, such as:

    • user can be abbreviated to u;
    • userID can be abbreviated to uid.
  • When using proper nouns, the following rules should be followed:

    • If the variable is private and the proper noun is the first word, use lowercase, such as apiClient.
    • Otherwise, use the original spelling of the noun, such as APIClient, repoID, UserID.

Here are some common proper nouns:

// A GonicMapper that contains a list of common initialisms taken from golang/lint
var LintGonicMapper = GonicMapper{
    "API":   true,
    "ASCII": true,
    "CPU":   true,
    "CSS":   true,
    "DNS":   true,
    "EOF":   true,
    "GUID":  true,
    "HTML":  true,
    "HTTP":  true,
    "HTTPS": true,
    "ID":    true,
    "IP":    true,
    "JSON":  true,
    "LHS":   true,
    "QPS":   true,
    "RAM":   true,
    "RHS":   true,
    "RPC":   true,
    "SLA":   true,
    "SMTP":  true,
    "SSH":   true,
    "TLS":   true,
    "TTL":   true,
    "UI":    true,
    "UID":   true,
    "UUID":  true,
    "URI":   true,
    "URL":   true,
    "UTF8":  true,
    "VM":    true,
    "XML":   true,
    "XSRF":  true,
    "XSS":   true,
}
  • If a variable is of type bool, the name should start with Has, Is, Can or Allow, for example:
var hasConflict bool
var isExist bool
var canManage bool
var allowGitHook bool
  • Local variables should be as short as possible, for example, using buf for buffer and i for index.
  • Code-generated files are exceptions to this rule (e.g., Id in xxx.pb.go).

2.7 Constant Naming #

  • Constant names must follow camel case, with the first letter capitalized or lowercase depending on the access control.
  • If the constant is an enumeration, the corresponding type should be created first:
// Code defines an error code type.
type Code int

// Internal errors.
const (
    // ErrUnknown - 0: An unknown error occurred.
    ErrUnknown Code = iota
    // ErrFatal - 1: A fatal error occurred.
    ErrFatal
)

2.8 Naming Errors #

  • Error types should be named in the form FooError.
type ExitError struct {
    // ...
}
  • Error variables should be named in the form ErrFoo.
var ErrFormat = errors.New("unknown format")

3. Comment Guidelines #

  • Every exportable name must have a comment that provides a brief introduction to the exported variable, function, struct, interface, etc.
  • Single-line comments should be used exclusively; multi-line comments are prohibited.
  • Just like with code, single-line comments should not exceed 120 characters. If they do, they should be broken into multiple lines while maintaining a clean format.
  • Comments must be complete sentences, with the thing being commented on as the starting point and a period as the ending point. The format should be // Name Description. For example:
// bad
// logs the flags in the flagset.
func PrintFlags(flags *pflag.FlagSet) {
    // normal code
}

// good
// PrintFlags logs the flags in the flagset.
func PrintFlags(flags *pflag.FlagSet) {
    // normal code
}
  • All commented-out code should be deleted before submitting the code for review. If there is a reason not to delete it, an explanation should be provided along with suggestions for further processing.

  • Blank lines can be used to separate multiple paragraphs of comments, as shown below:

// Package superman implements methods for saving the world.
//
// Experience has shown that a small number of procedures can prove
// helpful when attempting to save the world.
package superman

3.1 Package Comments #

  • Each package must have one and only one package-level comment.
  • Package-level comments should be preceded by // and have the following format: // Package PackageName PackageDescription. For example:
// Package genericclioptions contains flags which can be added to you command, bound, completed, and produce
// useful helper functions.
package genericclioptions

3.2 Variable/Constant Comments #

  • Every exportable variable/constant must have a comment that explains its purpose. The format should be // VariableName VariableDescription. For example:
// ErrSigningMethod defines invalid signing method error.
var ErrSigningMethod = errors.New("Invalid signing method")
  • When there are large blocks of constant or variable definitions, it is acceptable to provide an overall comment before the block and then provide detailed comments for each constant on the line preceding or following its definition. For example:
// Code must start with 1xxxxx.
const (
    // ErrSuccess - 200: OK.
    ErrSuccess int = iota + 100001

    // ErrUnknown - 500: Internal server error.
    ErrUnknown

    // ErrBind - 400: Error occurred while binding the request body to the struct.
    ErrBind

    // ErrValidation - 400: Validation failed.
    ErrValidation
)

3.3 Struct Comments #

  • Every exported struct or interface must have a comment that explains its purpose. The format should be // StructName StructDescription.
  • If the meaning of an exported member variable inside a struct is not clear, it must be accompanied by a comment on the line before or at the end of the member variable. For example:
// User represents a user restful resource. It is also used as gorm model.
type User struct {
    // Standard object's metadata.
    metav1.ObjectMeta `json:"metadata,omitempty"`

    Nickname string `json:"nickname" gorm:"column:nickname"`
    Password string `json:"password" gorm:"column:password"`
    Email    string `json:"email" gorm:"column:email"`
    Phone    string `json:"phone" gorm:"column:phone"`
    IsAdmin  int    `json:"isAdmin,omitempty" gorm:"column:isAdmin"`
}

3.4 Method Comments #

  • Every exported function or method must have a comment that explains its purpose. The format should be // FunctionName FunctionDescription. For example:
// BeforeUpdate run before update database record.
func (p *Policy) BeforeUpdate() (err error) {
    // normal code
    return nil
}

3.5 Type Comments #

  • Every exported type definition and alias must have a comment that explains its purpose. The format should be // TypeName TypeDescription. For example:
// Code defines an error code type.
type Code int

4. Types #

4.1 Strings #

  • Checking an empty string.
// bad
if s == "" {
    // normal code
}

// good
if len(s) == 0 {
    // normal code
}
  • Comparing []byte and string.
// bad
var s1 []byte
var s2 []byte
...
bytes.Equal(s1, s2) == 0
bytes.Equal(s1, s2) != 0


// good
var s1 []byte
var s2 []byte
...
bytes.Compare(s1, s2) == 0
bytes.Compare(s1, s2) != 0
  • Using raw string for complex strings to avoid character escaping.
// bad
regexp.MustCompile("\\.")

// good
regexp.MustCompile(`\.`)

4.2 Slices #

  • Checking an empty slice.
// bad
if len(slice) = 0 {
    // normal code
}

// good
if slice != nil && len(slice) == 0 {
    // normal code
}

The above applies to maps and channels as well.

  • Declaring a slice.
// bad
s := []string{}
s := make([]string, 0)

// good
var s []string
  • Copying a slice.
// bad
var b1, b2 []byte
for i, v := range b1 {
   b2[i] = v
}
for i := range b1 {
   b2[i] = b1[i]
}

// good
copy(b2, b1)
  • Adding elements to a slice.
// bad
var a, b []int
for _, v := range a {
    b = append(b, v)
}

// good
var a, b []int
b = append(b, a...)

4.3 Structs #

  • Initializing a struct.

Initialize structs in a multiline format.

type user struct {
    Id   int64
    Name string
}

u1 := user{100, "Colin"}

u2 := user{
    Id:   200,
    Name: "Lex",
}

5. Control Structures #

5.1 if #

  • The if statement accepts initialization statements in the following way to create local variables.
if err := loadConfig(); err != nil {
	// error handling
	return err
}
  • For variables of type bool, the if statement should make a direct truth/false judgement.
var isAllow bool
if isAllow {
	// normal code
}

5.2 for #

  • Use short declaration to create local variables.
sum := 0
for i := 0; i < 10; i++ {
    sum += 1
}
  • Do not use defer within a for loop as defer will only execute when the function exits.
// bad
for file := range files {
	fd, err := os.Open(file)
	if err != nil {
		return err
	}
	defer fd.Close()
	// normal code
}

// good
for file := range files {
	func() {
		fd, err := os.Open(file)
		if err != nil {
			return err
		}
		defer fd.Close()
		// normal code
	}()
}

5.3 range #

  • If you only need the first item (key), discard the second item.
for key := range keys {
	// normal code
}
  • If you only need the second item, assign an underscore to the first item.
sum := 0
for _, value := range array {
	sum += value
}

5.4 switch #

  • switch statements must have a default case.
switch os := runtime.GOOS; os {
    case "linux":
        fmt.Println("Linux.")
    case "darwin":
        fmt.Println("OS X.")
    default:
        fmt.Printf("%s.\n", os)
}

5.5 goto #

  • Avoid using goto in business code.
  • Frameworks or other low-level source code should also avoid using goto.

6. Functions #

  • Function names should start with lowercase letters for both input variables and return variables.

  • Functions should have no more than 5 parameters.

  • Function grouping and ordering

    • Functions should be sorted in a rough order of invocation.
    • Functions in the same file should be grouped by receiver.
  • Prefer value passing over pointer passing.

  • When the input is a map, slice, channel, or interface, do not pass a pointer.

6.1 Function Parameters #

  • Use named returns if the function returns two or three parameters of the same type, or if the meaning of the results is not clear from the context. Otherwise, named returns are not recommended. For example:

    func coordinate() (x, y float64, err error) { // normal code }

  • Both input variables and return variables should start with lowercase letters.

  • Prefer value passing over pointer passing.

  • The number of parameters should not exceed 5.

  • Functions with multiple return values should return at most three values. If there are more than three values, consider using a struct.

6.2 defer #

  • When there is resource creation, defer should be used to release the resource immediately (defer can be used liberally, as defer performance has been greatly improved in Go 1.14, and the performance cost of defer can be ignored even in performance-sensitive businesses).

  • Check for errors first before deferring the resource release. For example:

    resp, err := http.Get(url) if err != nil { return err }

    defer resp.Body.Close()

6.3 Method Receivers #

  • It is recommended to use the lowercase first character of the class name as the receiver name.
  • Do not use single characters for receiver names when the function exceeds 20 lines.
  • Avoid using easily confused names such as “me”, “this”, or “self” for receiver names.

6.4 Nesting #

  • The nesting depth should not exceed 4 levels.

6.5 Variable Naming #

  • Variable declarations should be placed before the first usage of the variable, following the principle of proximity.

  • If a magic number appears more than twice, it should be replaced with a constant. For example:

    // PI … const Prise = 3.14

    func getAppleCost(n float64) float64 { return Prise * n }

    func getOrangeCost(n float64) float64 { return Prise * n }

7. GOPATH Setting Guidelines #

  • Starting from Go 1.11, the GOPATH rule has been relaxed. Existing code (including many libraries created before 1.11) certainly adheres to this rule. It is recommended to keep the GOPATH rule for better code maintenance.
  • It is recommended to use only one GOPATH and not to use multiple GOPATHs. If multiple GOPATHs are used, the “bin” directory in the first GOPATH will be the effective one for compilation.

8. Dependency Management #

  • Starting from Go 1.11, it is mandatory to use Go Modules.
  • When using Go Modules as the dependency management tool, it is not recommended to commit the vendor directory.
  • When using Go Modules as the dependency management tool, it is mandatory to commit the go.sum file.

9. Best Practices #

  • Minimize the use of global variables and pass variables as arguments to make each function “stateless”. This reduces coupling and makes it easier for division of work and unit testing.
  • Validate interface compliance during compilation, for example:
type LogHandler struct {
  h   http.Handler
  log *zap.Logger
}
var _ http.Handler = LogHandler{}
  • When handling server requests, create a context to store relevant information (such as requestID) and pass it along the function call chain.

9.1 Performance #

  • Strings represented as string variables are immutable, and modifying strings is an expensive operation that usually requires memory reallocation. Therefore, if there is no special need, use []byte for modifications.
  • Prioritize using strconv instead of fmt.

9.2 Considerations #

  • Be careful with automatic memory allocation when using append, as it may return a newly allocated address.
  • If you want to directly modify the value of a map, the value must be a pointer; otherwise, you need to overwrite the original value.
  • Maps need to be locked when used in concurrency.
  • During compilation, it is impossible to check the conversion of interface{} types, and it can only be checked at runtime. Be careful to avoid causing panics.

Summary #

In this lecture, I introduced nine commonly used coding standards to you. However, at the end of today’s session, I want to remind you: standards are set by people, and you can also establish standards that suit your project’s needs. This is the mindset that I have been emphasizing in previous courses. At the same time, I also suggest that you adopt the standards that have been accumulated by the industry and use tools to ensure their implementation.

That is all for today’s content. Feel free to share your thoughts in the comments section below. See you in the next lecture.