编写地道的Go代码:第一章 命名规范

Grady Booch 是一位在软件工程领域做出了杰出贡献的计算机科学家。他通过开发 UML、提出 Booch 方法以及撰写畅销书等方式,极大地推动了软件工程领域的发展,并对今天的软件开发实践产生了深远的影响。他关于整洁代码(clean code)一段描述非常得到大家的赞同:

清晰的代码,简练而直白,读之如行云流水般的优美散文。它绝不故弄玄虚,而是以简洁的抽象和清晰的逻辑,淋漓尽致地展现设计者的匠心。

这个系列就是介绍Go语言中几百个编写整洁代码的建议。我不把它们称作为编码规范或者最佳实践,而是把它们称之为建议,原因在于有些建议并不一定强调开发者必须遵守,而是在大部分情况下,这些建议的做法比较符合地道的Go编程写法。

我在实际的工作中,也看到很多初入Go语言领域编写应用程序的同学,或许他刚入门或者从其他语言刚转入到Go一两年,得益于 Go语言的简单易学,就非常快的编写了Go可运行的程序,这是非常好的一件事情,但是也看到代码中一些不是那么地道的Go语言写法,比如变量的命名很奇怪、打印error信息首字母大写等等,让人很容易识别出是Go语言初学者的痕迹。

所以我想通过一个简短的课程,给大家介绍一下Go语言中如何编写干净整洁,逻辑清晰的代码,让你的代码看起来就像专家写的。

Go标准库是学习这些规范的最佳代码。

首先第一课就是命名规范。

编程中的命名规范是一套规则,用于为代码中的各种元素(如变量、函数、类、文件等)选择名称。良好的命名规范可以提高代码的可读性、可维护性和团队协作效率。

但是,做到好命名并不是一件容易的事。

There are only two hard things in Computer Science: cache invalidation and naming things. – Phil Karlton

计算机科学中最难的两件事:缓存失效和命名。

基础规则

1、使用驼峰命名法,公开函数首字母大写,私有函数首字母小写

这是 Go 语言中最基本的命名约定。公开的函数、类型、变量等可以被其他包访问,因此首字母需要大写;私有的则只能在包内部使用,首字母小写。

示例:

  • 公开函数:io.Copy()

  • 私有类型:io.nopCloser

2、接口名应以 -er 结尾,表示行为,如Reader、Writer

通过 -er 后缀,可以直观地表明这是一个行为接口;符合英语语言习惯,容易理解和记忆;清晰地表达"能做什么"的概念。

示例:

  • io.Reader:定义了 Read() 方法,表示可以读取数据的对象

  • io.Writer:定义了 Write() 方法,表示可以写入数据的对象

  • Closer:定义了 Close() 方法,表示可关闭的对象。

  • Stringer:定义了 String() 方法,表示可以格式化成字符串的对象。

3、变量名应该简短且有意义

变量名应该清晰地表达变量的用途,避免使用晦涩的缩写或无意义的名称。循环计数器通常可以使用 ijk 等单字母。特定的参数和变量也可以使用单字符,如rw

示例:

  • 临时变量wt, 如wt, ok := src.(WriterTo)

  • 简短有意义的变量size: size := 32 * 1024

  • 循环中的变量ifor i := 0; i < len(data); i++

4、包名使用小写单词,不使用下划线或混合大小写

包名应该简洁明了,反映包的功能。尽量一个单词。

示例:

  • netbyteshttphashimage

  • 需要两个单词的,可以缩写,如bufioexpvarioutilstrconv

  • 不推荐,如credentialproviderserviceaccount

5、常量名使用驼峰法

这种命名方式可以清晰地将常量与变量区分开。

示例:

  • bufio.MaxScanTokenSizenet.IPv4lenio.SeekStart

  • 全大写加下划线分隔如MAX_CONNECTIONS并不是Go语言命名常量地道的写法

6、 名称要表明其用途,避免无意义的别名

示例:

// 不好的例子,x,y含义不清晰
func process(x, y int) int {
    return x * y
}


// 好的例子
func calculateArea(length, width int) int {
    return length * width
}

包名规范

7、包名应该简短、明确,比如net、http、bytes (同#4)

示例:

  • net: 这个包提供了网络相关的基本功能,例如TCP/IP、UDP、域名解析等。包名简洁明了,直接表达了其用途。例如,net.Dial() 函数用于建立网络连接。

  • http: 这个包实现了HTTP客户端和服务端。例如,http.ListenAndServe() 用于启动HTTP服务器,http.Get() 用于发送HTTP GET请求。同样,包名非常清晰地表明了其功能。

  • bytes: 这个包提供了操作字节切片的函数。例如,bytes.Buffer 类型用于高效地构建字节切片。包名简短且准确地描述了其操作的数据类型。

8、避免使用common、util这样笼统的包名

Go标准库中没有使用 commonutil 这样的包名。这是因为这些名称过于宽泛,无法清晰地表达包的功能。如果需要一些通用的工具函数,通常会根据其具体用途放到更具针对性的包中,或者如果确实非常通用,会考虑是否可以合并到已有的相关包里。

示例:

  • ioutil是和输入输出相关的工具包

  • httputil是和http相关的工具包

9、 包名和目录名保持一致

Go的包管理机制要求包名和目录名保持一致。例如,net 包的代码位于 $GOROOT/src/net 目录下,http 包的代码位于 $GOROOT/src/net/http 目录下(注意http是net的子包)。这种约定使得代码组织清晰,易于查找和维护。

10、避免包名冲突,必要时使用重命名导入

虽然Go的包路径包含了完整的导入路径(例如 net/http),但仍然可能存在不同模块的包名相同的情况。这时可以使用重命名导入来解决冲突。

示例:

import (
    "fmt"
    myhttp "my.org/http" // 将 my.org/http 包重命名为 myhttp
    stdhttp "net/http"   // 将 net/http 包重命名为 stdhttp
)


func main() {
    stdhttp.Get("http://example.com")
    myhttp.MyFunction()
}

11、避免包名和内建类型冲突,可以使用复数

示例:

  • bytes: 操作字节切片。

  • strings: 操作字符串。

  • maps: 扩展的map操作。

  • slices: 扩展的slice操作。

变量命名

12、 bool类型变量名应带有ishascan等前缀

使用这些前缀可以清晰地表达变量的布尔含义。例如:

  • isValid:表示某个值是否有效。

  • hasPermission:表示是否拥有某个权限。

  • canAccess:表示是否可以访问某个资源。

  • isFinished:表示某个操作是否完成。

  • hasError:表示是否发生错误。

13、 切片名称用复数形式,如usersports

使用复数形式可以清晰地表明变量存储的是一个集合。例如:

  • users:存储用户信息的切片。

  • ports:存储端口号的切片。

  • messages:存储消息的切片。

  • results:存储结果的切片。

14、 指针变量不需要带ptr前缀

Go语言中,类型信息已经足够表明一个变量是否为指针,因此不需要额外的 ptr 前缀。直接使用变量名即可,例如:

var user *User
user = &newUser
name := user.Name // 直接使用 user.Name,无需 ptrUser.Name

15、 接收者名称应简短,通常用类型首字母,再不济用两个字母简写

在方法定义中,接收者名称应该简短,通常使用类型名的首字母。例如:

type User struct {
    Name string
}


func (u User) GetName() string { // 接收者 u
    return u.Name
}


type File struct {
    fd int
}


func (f File) Close() error { // 接收者 f
    // ...
}

16、 中间结果用有意义的名称,避免result1result2

使用有意义的名称可以提高代码的可读性。例如,不要使用:

result1 := calculateValue(x)
result2 := processResult(result1)

而应该使用:

calculatedValue := calculateValue(x)
processedValue := processResult(calculatedValue)

或者更具上下文的简短名称:

sum := calculateSum(numbers)
average := calculateAverage(sum, len(numbers))

17、 全局变量一般不添加前缀

不像其他编程语言,Go语言一般不为全局变量添加前缀。例如下面的写法在地道的Go程序较少见:

var gLogger *log.Logger
var gConfig *Config


func init() {
    gLogger = log.New(os.Stdout, "MYAPP: ", log.LstdFlags)
    gConfig = loadConfig()
}

18、 接口实现者命名可以使用接口名做后缀

这是一个常见的约定,可以清晰地表明某个类型实现了哪个接口。例如:

  • io.Reader 接口的实现类型可以命名为 FileReaderBufReaderStringReader 等。

  • http.Handler 接口的实现类型可以命名为 MyHandlerUserHandler 等。

函数/方法命名

19、Set前缀用于属性访问,一般不使用GetXXX(),而是直接XXX()命名属性读取方法

在Go中,对于属性的设置使用 Set 前缀,而读取则直接使用属性名作为方法名。例如:

func (c *IPConn) SetDeadline(t time.Time) error
func (c *IPConn) SetReadBuffer(bytes int) error
func (c *IPConn) SetReadDeadline(t time.Time) error
func (c *IPConn) SetWriteBuffer(bytes int) error
func (c *IPConn) SetWriteDeadline(t time.Time) error


func (a *TCPAddr) AddrPort() netip.AddrPort
func (a *TCPAddr) Network() string
func (a *TCPAddr) String() string

20、New前缀用于构造函数

使用 New 前缀的函数用于创建类型的实例。

包下面如果只有一个构造函数,那么可以只定义New,如果有多个构造函数,可以使用NewXXXXX明确特定类型的构造函数:

func New(text string) error
func NewReader(rd io.Reader) *Reader
func NewReaderSize(rd io.Reader, size int) *Reader

21、Parse前缀用于解析操作

使用 Parse 前缀的函数用于将字符串或其他格式的数据解析成某种类型。例如:

num, err := strconv.ParseInt(str, 10, 64)

22、Handle前缀用于处理回调

使用 Handle 前缀的函数用于处理事件或回调。例如:

func HandleFunc(pattern string, handler func(ResponseWriter, *Request))

23、 测试函数用Test前缀加被测函数名

Go的测试框架要求测试函数以 Test 开头,后面是被测函数的名称。例如:

func Add(a, b int) int {
    return a + b
}


func TestAdd(t *testing.T) {
    if Add(2, 3) != 5 {
        t.Error("Add(2, 3) should be 5")
    }
}

24、 基准测试用Benchmark前缀

基准测试函数以 Benchmark 开头。例如:

func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Add(1, 1)
    }
}

25、 方法名应体现动作或者功能,如convertToString而不是conversion,除非是读取属性方法

方法名应该清晰地表达方法的功能。使用动词或动词短语,例如:

  • convertToString():将某个值转换为字符串。

  • calculateSum():计算总和。

  • sendMessage():发送消息。

而不是使用名词,例如 conversioncalculationmessage。但是读取属性的方法可以直接使用属性名,如前文所述的 Name()

26. 避免无意义的命名,比如仅仅使用HandleProcessDo,标准库这样写除外

除非在非常通用的上下文中,否则应避免使用过于笼统的命名,例如仅仅使用 HandleProcessDo。应该根据具体的功能使用更具描述性的名称。例如,与其使用 HandleData(),不如使用 ProcessUserData()ValidateInputData()。标准库中一些非常底层的包,由于其通用性,可能会使用这些简单的命名,如func (c *Client) Do(req *Request) (*Response, error),但一般情况下我们应该避免。

27、和第12条一样, 返回值是bool类型的函数名应带有ishascan等前缀

个别函数例外,如Equal

28、函数一般使用动词原型

比如使用Equal而不是Equals,使用Get而不是Gets

29、单词倾向于使用美式写法

美式拼写英式拼写
Marshal, UnMarshalMarshall, Unmarshall
color, flavor, neighbor, rumorcolour, flavour, neighbour, rumour
center, fiber, metercentre, fibre, metre
analyze, organize, realizeanalyse/analyze, organise/organize, realise/realize
traveling, traveling, labeledtravelling, travelling, labelled
program, draftprogramme, draught
adapteradaptor

结构体命名

30、名称应该是名词或名词短语

结构体的名称应该清晰地描述它所代表的事物或概念,因此应该使用名词或名词短语。例如:

  • User:表示一个用户。

  • Product:表示一个产品。

  • Order:表示一个订单。

  • CustomerAddress:表示客户地址。

  • FileData:表示文件数据。

避免使用动词或形容词作为结构体名称,例如 RunningProcessImportantData

31、尽量使用单个单词,适当采用缩写(众所周知的缩写)

能用一个单词能表达的就用一个单词,或者一个单词加实现的接口名,杜绝非常多的单词组成的结构名。

广为人知的缩写可以使用,例如 HTMLURLIDAPI 等。

一些结构体名称:

  • Buffer

  • LimitedReader

  • HTMLDoc而不是HyperTextMarkupLanguageDocument

  • Cond而不是ConditionVariable

32、保持一致性

在一个项目中,应该保持结构体命名的一致性。例如,如果使用 User 表示用户,则不要在其他地方使用 PersonMember 表示相同的概念。

接口命名

33、多方法接口用名词描述其功能

如果一个接口包含多个方法,则应该使用名词或形容词来描述接口的功能或特性。例如:

  • ReadWriteCloser:包含 Read()Write()Close() 方法。

  • HTTPHandler:处理 HTTP 请求的接口。

  • Encoding:定义编码/解码操作的接口。

  • Iterator:定义迭代器行为的接口。

  • Ordered:定义了可排序行为的接口。这里的Ordered更多的是名词的意思(OrderedObject)

避免在这种情况下使用 动词+er 的形式,因为这可能会导致接口名称过于冗长或难以理解。 只对单一方法接口的命名动词+er采用形式。

较少使用形容词,比如优先使用Iterator,而不是Iterable

34、避免使用IInterface前缀

在 Go 语言中,不推荐使用 IInterface 前缀来命名接口。例如,不要使用 IReaderReaderInterface,而应该直接使用 Reader。Go 语言的类型系统已经足够清晰,不需要额外的 IInterface 前缀来表明一个类型是接口。

类型别名命名

35、如果是包级别的类型,名称要足够特异

对于包级别的类型别名,名称尤其需要具有独特性,以避免与其他包或类型产生混淆。这意味着名称应该清晰地表达别名所代表的类型,并尽可能避免使用过于通用的名称:

package network


import "net/http"


// Bad: MyCustomRequest 是 http.Request 的类型别名,用于表示自定义的请求类型
// 仅仅使用 MyCustomRequest 可能不够清晰,因为其他包也可能定义类似的别名。
type MyCustomRequest = http.Request


// 更好的方式:添加更具特异性的前缀或后缀
type NetworkRequest = http.Request
type InternalRequest = http.Request //如果仅在包内部使用

36、考虑添加适当后缀表明其特性

为了进一步提高类型别名的可读性和可理解性,可以考虑添加适当的后缀来表明其特性或用途。

一些常用的后缀包括:

  • Type: 用于强调这是一个类型别名。例如 RequestType

  • Alias: 明确表明这是一个别名。例如 RequestAlias

  • Func: 用于函数类型别名。例如 HandlerFunc

  • List: 用于表示某种类型的列表。例如 UserList (虽然通常用 []User)

嵌入字段命名

37、匿名嵌入使用类型名称

当一个类型被匿名地嵌入到另一个类型中时,该类型的名称就成为了嵌入字段的名称。这意味着你可以直接通过外部类型的实例访问嵌入类型的字段和方法。

type Address struct {
    Street  string
    City    string
    ZipCode string
}


type User struct {
    Name    string
    Address // 匿名嵌入 Address 类型
}

38、命名嵌入使用有意义的字段名

当使用命名嵌入时,需要为嵌入的类型指定一个字段名。这个字段名应该清晰地描述嵌入类型的用途或角色。

type Author struct {
    Name string
    Bio  string
}


type Book struct {
    Title  string
    Author Author `json:"author"` // 命名嵌入 Author 类型,字段名为 author
}

39、 避免因嵌入导致的名称冲突

当外部类型和嵌入类型具有相同的字段或方法名时,会发生名称冲突。在这种情况下,外部类型的字段或方法会“屏蔽”嵌入类型的同名成员。为了避免混淆,应该尽量避免这种冲突。

type Inner struct {
    Value int
    Name string
}


type Outer struct {
    Value string
    Inner
}


func main() {
    outer := Outer{
        Value: "outer value",
        Inner: Inner{
            Value: 10,
            Name: "inner name",
        },
    }


    println(outer.Value) // 输出:outer value (Outer 的 Value 屏蔽了 Inner 的 Value)
    println(outer.Inner.Value) // 输出:10,需要通过Outer.Inner.Value访问
    println(outer.Name) // 输出:inner name
}

40、考虑可见性要求选择大小写

和其他标识符一样,嵌入字段的可见性也由其首字母的大小写决定。

  • 首字母大写的嵌入字段是导出的(public),可以被其他包访问。

  • 首字母小写的嵌入字段是非导出的(private),只能在当前包内部访问。

Error类型命名

41、 错误变量以ErrError为前缀

在包级别定义的错误变量(通常是哨兵错误),应该以 ErrError 为前缀。这有助于清晰地标识这些变量表示的是错误。

var ErrClosedPipe = errors.New("io: read/write on closed pipe")

42、自定义错误类型以Error为后缀

当需要定义包含更多信息的自定义错误类型时,应该使用结构体或其他类型,并实现 error 接口。自定义错误类型的名称应该以 Error 为后缀。

type InputError struct{ ... }


type onceError struct {
	sync.Mutex // guards following
	err        error
}

43、wrap 错误时保持原始错误信息的语义

使用 fmt.Errorf%w 动词或第三方库(如 errors 包)进行错误包装时,应该尽量保持原始错误信息的语义,避免信息丢失或误导。

package mypackage


import (
    "fmt"
    "os"
)


func ReadFile(filename string) error {
    _, err := os.ReadFile(filename)
    if err != nil {
        // 正确的 wrap 方式,保留了原始错误信息
        return fmt.Errorf("reading file %s: %w", filename, err)


        // 不好的 wrap 方式,丢失了原始错误信息
        // return fmt.Errorf("failed to read file")
    }
    return nil
}

正确的 wrap 方式保留了 os.ReadFile 返回的原始错误信息,方便调试和排查问题。

44、 错误字符串首字母不大写,不用标点结尾

错误字符串应该使用小写字母开头,并且不要使用标点符号结尾。这是 Go 语言的惯例。

package mypackage


import "errors"


var ErrInvalidInput = errors.New("invalid input") // 正确:小写开头,没有标点


// 不好的例子:
// var ErrInvalidInput = errors.New("Invalid input.") // 大写开头,有句点
// var ErrInvalidInput = errors.New("INVALID INPUT") // 全部大写

常量命名

Go 语言中,常量使用 const 关键字声明。常量名通常使用驼峰式,而不是全大写加下划线。

45、枚举常量使用分组前缀,如ColorRedColorBlue

当定义一组相关的常量,用于表示枚举值时,应该使用一个共同的前缀来分组这些常量。这有助于清晰地标识这些常量属于同一个枚举类型。

package mypackage


const (
    ColorRed   = 1
    ColorGreen = 2
    ColorBlue  = 3
)


const (
    FileModeRead  = 0400
    FileModeWrite = 0200
    FileModeExec  = 0100
)

除非是众所周知的常量,如星期名,月份名、性别等等。

46、状态常量用State前缀,如StateActive

当定义一组常量来表示某个对象或系统的不同状态时,应该使用 State 前缀。

package mypackage


const (
    StateActive   = 1
    StateInactive = 2
    StatePending  = 3
)

在这个例子中,StateActiveStateInactiveStatePending 使用了 State 前缀,清晰地表明它们表示不同的状态。

47、配置相关常量用Config前缀

当定义一些用于配置系统的常量时,应该使用 Config 前缀。

package mypackage


const (
    ConfigMaxConnections = 100
    ConfigDefaultTimeout = 30 // seconds
)

48、魔法数字转为常量时用MaxMinDefault等前缀

当代码中出现一些难以理解的“魔法数字”时,应该将其定义为常量,并使用 MaxMinDefault 等前缀来描述其含义。

package mypackage


const (
    MaxUserNameLength = 50
    MinPasswordLength = 8
    DefaultPageSize   = 20
)


// 不好的例子:直接使用魔法数字
// if len(userName) > 50 { ... }


// 好的例子:使用常量
if len(userName) > MaxUserNameLength {
    // ...
}

49、 私有常量可以使用小写,但要保持包内一致性

虽然 Go 语言的惯例是使用大写字母命名常量,但对于只在包内部使用的私有常量,可以使用小写字母。但是,在一个包内应该保持一致性,要么全部使用大写,要么全部使用小写。推荐使用大写,除非有特殊原因。

package mypackage


const (
    maxInternalValue = 100 // 私有常量,小写
    MinInternalValue = 0 // Bad: 同一个包内,也应该用小写,保持一致性
)

枚举类型命名

50、定义一个基础类型

需要定义一个基础类型来表示枚举值。通常使用整数类型(如 intint8int16int32int64)或字符串类型。

  • 使用整数类型: 如果枚举值只是简单的标识符,没有其他关联的数据,则通常使用整数类型。

  • 使用字符串类型: 如果枚举值需要更具描述性的字符串表示,或者需要与其他系统进行交互,则可以使用字符串类型。

51、使用常量组定义枚举值

使用 const 关键字和分组声明来定义枚举值。这有助于将相关的常量组织在一起,提高代码的可读性。

type Color int


const (
    ColorRed Color = iota // 0
    ColorGreen        // 1
    ColorBlue         // 2
)


// 使用 iota 的偏移量
type ByteSize float64


const (
    _           = iota // ignore first value by assigning to blank identifier
    KB ByteSize = 1 << (10 * iota)
    MB
    GB
    TB
    PB
    EB
    ZB
    YB
)

52、使用分组前缀

使用一个共同的前缀来分组枚举常量,清晰地表明它们属于同一个枚举类型。

type Color int


const (
    ColorRed   Color = iota
    ColorGreen
    ColorBlue
)

53、为枚举类型定义 String() 方法

为了方便打印和调试,应该为枚举类型定义 String() 方法,使其能够返回枚举值的字符串表示。

package main


import "fmt"


type Color int


const (
    ColorRed   Color = iota
    ColorGreen
    ColorBlue
)


func (c Color) String() string {
    switch c {
    case ColorRed:
        return "Red"
    case ColorGreen:
        return "Green"
    case ColorBlue:
        return "Blue"
    default:
        return fmt.Sprintf("Color(%d)", c) // 处理未知值
    }
}


func main() {
    fmt.Println(ColorRed)   // 输出: Red
    fmt.Println(ColorGreen) // 输出: Green
    fmt.Println(ColorBlue)  // 输出: Blue
    fmt.Println(Color(4)) // 输出: Color(4)
}

可以使用dmarkham/enumerabice/go-enum等一些工具辅助代码生成。

54、使用类型别名提高可读性

可以使用类型别名来简化枚举类型的声明,尤其是在枚举类型名较长的情况下。

type AudioSampleRate uint32


const (
    AudioSampleRate44100 AudioSampleRate = 44100
    AudioSampleRate48000 AudioSampleRate = 48000
    // ...
)


// 可以使用类型别名简化
type ASR = AudioSampleRate


const (
    ASR44100 ASR = 44100
    ASR48000 ASR = 48000
)

55、避免使用零值作为有效的枚举值

通常情况下,应该避免使用零值作为有效的枚举值,以便能够区分未初始化的枚举值和有效的枚举值。如果需要表示“未设置”或“默认”状态,可以定义一个特殊的枚举值,例如 ColorUnknownStatusDefault

56、使用_跳过iota的某些值

可以使用空白标识符 _ 来跳过 iota 生成的某些值。

type SpecialErrorCode int


const (
    _                SpecialErrorCode = iota // 0 (被跳过)
    ErrorCodeOne                           // 1
    ErrorCodeTwo                           // 2
    ErrorCodeThree                         // 3
)

注释

57、文件注释

位于文件顶部,描述文件的用途、作者、版权信息等。

常常使用/* ... */且多行的形式。

/*
Copyright 2024 The MyProject Authors. All rights reserved.
Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file.
*/


package mypackage

58、包注释

位于 package 声明之前,简要描述包的功能,以单词Package开始,后接包名。

// Package mypackage provides functions for ...
package mypackage

59、函数/方法注释(文档注释)

位于函数或方法声明之前,使用 ///* ... */,但推荐使用 // 以便 godoc 工具生成文档。

以函数名或者方法名开头。

// Add adds two integers and returns their sum.
//
// Example:
//    sum := Add(1, 2) // sum is 3
//
// Parameters:
//    a: The first integer.
//    b: The second integer.
//
// Returns:
//    The sum of a and b.
func Add(a, b int) int {
    return a + b
}

60、类型注释

位于类型声明之前,以类型名开头,常常使用 //

// User represents a user in the system.
type User struct {
    Name string
    Age  int
}

61、代码块注释

解释一段代码的逻辑, 常常使用 //

// Calculate the average of the numbers.
sum := 0
for _, num := range numbers {
    sum += num
}
average := float64(sum) / float64(len(numbers))

62、行尾注释

位于代码行尾,对该行代码进行简短的解释,如无特殊需求推荐使用 //

x := 10 // Initialize x to 10

63、通用注释规则

  • 空格:

    • 单行注释 // 后应有一个空格,例如 // This is a comment

    • 多行注释 /* ... */ 的开始和结束符号与注释内容之间也应有空格,例如 /* This is a multi-line comment */

  • 句点(结尾标点):

    • 函数/方法、类型、包、文件注释等描述性注释,推荐使用完整的句子,以句点结尾。这有助于提高可读性,尤其是在使用 godoc 生成文档时。

    • 代码块注释和行尾注释可以根据需要使用句子片段或短语,句点不是强制的,但保持一致性很重要。

  • 首字母大小写:

    • 函数/方法、类型、包、文件注释等描述性注释,首字母应大写

    • 代码块注释和行尾注释可以根据需要使用大小写,但保持一致性很重要。

  • 缩进: 注释的缩进应与其所注释的代码保持一致,以提高代码的整体可读性。

  • 避免嵌套/* ... */注释: C 风格的多行注释不支持嵌套。如果需要注释掉一段包含多行注释的代码,可以使用 // 进行单行注释。

  • 函数名和方法名一般没有参数和返回值的列表解释,而是针对特殊情况进行简单解释。有些公司要求提供参数和返回值的格式说明,这不是Go地道的写法。