Go语言编码规范

一、 版本记录

二、 编码风格规范

2.1、 格式化

2.1.1、 缩进

2.1.2、 行长度

2.1.3、 括号

2.2、 命名规则

2.2.1、 包名

2.2.2、 函数名、方法名

2.2.3、 结构体名

2.2.4、 接口名

2.2.5、 变量名

2.2.6、 常量名

2.3、 变量、常量定义

2.4、 变量类型定义

2.4.1、 String 类型定义

2.4.2、 Slice、Map 类型定义

2.4.3、 结构体定义

2.5、 接口定义

2.6、 函数、方法定义

2.7、 错误处理

2.8、 其他说明

三、 注释规范

3.1、 注释风格

3.2、 包注释

3.3、 函数、方法注释

3.4、 结构体、接口注释

3.5、 其他说明

一、 版本记录

|------------|------|------|-----|-----|
| 日期 | 版本 | 修改描述 | 修订者 | 审核者 |
| 2025-12-19 | v0.1 | 初始版本 | 研发部 | 研发部 |

二、 编码风格规范

2.1、 格式化

1、 go 中主要通过 go fmt 程序(也可以用 go fmt)将 go 程序按照标准风格缩进、对齐、保留注释并在需要时重新格式化。

2、 使用vscode,安装 go 拓展及 go tools 之后,即可自动格式化成标准风格。

2.1.1、 缩进

1、 go 中使用 tab 缩进,go fmt 也是默认使用 tab 。建议将 tab 设置为4个空格。

2.1.2、 行长度

1、 go 对行的长度没有限制。但是我们还是建议行的长度不要超过120,应该在达到这个限制之前换行。

2.1.3、 括号

1、 go 中的控制结构(if、for、switch)在语法上不需要圆括号。

2.2、 命名规则

2.2.1、 包名

1、 全部小写。没有大写或下划线,不建议用驼峰。

2、 大多数使用命名导入的情况下,不需要重名。

3、 简短而简洁,(如果你对缩写熟悉的话,可以在保证见名知意的前提下使用缩写)。

4、 不要用复数。

5、 不要用"common"、"util"、"lib"。这些不好,信息量不足。

2.2.2、 函数名、方法名

1、 遵循 go 社区对函数名的约定,使用驼峰风格(例如:MixedCaps 或者 mixedCaps )。

2、 规则:动词 + 名词。

3、 如果函数、方法名是判断类型(返回值主要为bool类型),则名称应以 Has、Is、Can 等判断性动词开头。

4、 注意:开头大写和小写,在 go 中的区别。

2.2.3、 结构体名

1、 结构体名应该是驼峰风格的名词或者名词短语。(例如:Custome、WikiPage、Account)

2.2.4、 接口名

2、 以"er"作为后缀,例如 Reader、 Writer、 Formatter、CloseNotifier 等。接口实现的方法则去掉"er",例如:Read、Write 等。

2.2.5、 变量名

1、 变量名遵循驼峰风格。

2、 首字母根据访问控制原则使用大写或小写。

3、 对于旨在本文件中有效的顶级变量,应该使用"_"前缀,避免在同一个包中的其他文件中意外使用错误的值。

4、 如果变量为 bool 类型,则名称应以 Has、Is、Can等开头。

2.2.6、 常量名

1、 常量全部大写,使用"_"分词。

2、 对于旨在本文件中有效的顶级常量,应该使用"_"前缀,避免在同一个包中的其他文件中意外使用错误的值。

3、 如果常量为 bool 类型,则名称应以 HAS、IS、CAN等开头。

2.3、 变量、常量定义

1、 常量只能是数字、字符、字符串或布尔值。枚举常量使用枚举器 iota 创建。

2、 枚举从1开始。除非0有意义

3、 因为变量的默认值为0, 因此通常以非零值开头枚举,以区分默认值。

4、 函数内使用短变量声明(:=);函数外使用长变量声明(var)。var 关键字一般用于包级别变量声明,或者函数内的零值情况。

5、 变量、常量的分组声明一般需要按照功能来区分,而不是将所有类型都分在一组,枚举常量,需要先创建相应类型:

Go 复制代码
type ParseError int

const (
    ERR_SECTION_NOT_FOUND ParseError = iota + 1 
    ERR_KEY_NOT_FOUND 
    ERR_BLANK_SECTION_NAME 
    ERR_COULD_NOT_PARSE
)

6、 通常情况下,尽量缩小变量的作用范围。

Go 复制代码
反例:
err := ioutil.WriteFile(name, data, 0644)
if err != nil {
    return err
}
正例:

if err := ioutil.WriteFile(name, data, 0644); err != nil {
    return err
}

7、 如果需要在 if 之外使用函数调用的结果,则不应尝试缩小变量的作用范围。

Go 复制代码
data, err := ioutil.ReadFile(name)
if err != nil { 
    return err
}

if err := cfg.Decode(data); err != nil {
    return err
}

fmt.Println(cfg)
return nil

2.4、 变量类型定义

2.4.1、 String 类型定义

1、 声明 Printf-style String 时,将其设置为 const 常量,有助于 go vet 对 String 类型实例执行静态分析。

Go 复制代码
const msg = "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2)

2、 优先使用 strconv 而不是 fmt, 将原语转换为字符串或从字符串转换时, strconv 速度比 fmt 快。

3、 避免字符串到字节的转换,不要反复从固定字符串创建字节 Slice,执行一次性完成转换。

Go 复制代码
反例:

for i := 0; i < b.N; i++ {
  w.Write([]byte("Hello world"))
}
正例:

data := []byte("Hello world")
for i := 0; i < b.N; i++ {
  w.Write(data)
}

2.4.2、 Slice、Map 类型定义

1、 尽可能指定容器的容量,以便为容器预先分配内存,向 make() 传入容量参数会在初始化时尝试调整 Slice、Map 类型实例的大小,这将减少在将元素添加到 Slice、Map 类型实例时的重新分配内存造成的损耗。

2、 使用 make() 初始化 Map 类型变量,使得开发者可以很好的区分开 Map 类型实例的声明,或初始化。使用 make() 还可以方便地添加大小提示。

3、 如果 Map 类型实例包含固定的元素列表,则使用 map literals(map 初始化列表)的方式进行初始化:

Go 复制代码
反例:

m := make(map[T1]T2, 3)
m[k1] = v1
m[k2] = v2
m[k3] = v3
正例:

m := map[T1]T2{
  k1: v1,
  k2: v2,
  k3: v3,
}

4、 在追加 Slice 类型变量时优先指定切片容量,在初始化要追加的切片时为 make() 提供一个容量值。

Go 复制代码
for n := 0; n < b.N; n++ {
  data := make([]int, 0, size)
  for k := 0; k < size; k++{
    data = append(data, k)
  }
}

5、 Map 或 Slice 类型实例是引用类型,所以在函数调用传递时,要注意在函数内外保证实例数据的安全性,除非你知道自己在做什么。这是一个深拷贝和浅拷贝的问题。

Go 复制代码
反例:

func (d *Driver) SetTrips(trips []Trip) {
  d.trips = trips
}
trips := ...
d1.SetTrips(trips)
// 你是要修改 d1.trips 吗?
trips[0] = ...
正例:

func (d *Driver) SetTrips(trips []Trip) {
  d.trips = make([]Trip, len(trips))
  copy(d.trips, trips)
}
trips := ...
d1.SetTrips(trips)

// 这里我们修改 trips[0],但不会影响到 d1.trips。
trips[0] = ...

6、 返回 Map 或 Slice 类型实例时,同样要注意用户对暴露了内部状态的实例的数值进行修改。

Go 复制代码
反例:

type Stats struct {
  mu sync.Mutex counters map[string]int
}

// Snapshot 返回当前状态。
func (s *Stats) Snapshot() map[string]int {
  s.mu.Lock()
  defer s.mu.Unlock() return s.counters
}

// snapshot 不再受互斥锁保护。
// 因此对 snapshot 的任何访问都将受到数据竞争的影响。
// 影响 stats.counters。·
snapshot := stats.Snapshot()
正例:

type Stats struct {
  mu sync.Mutex counters map[string]int
}

func (s *Stats) Snapshot() map[string]int {
  s.mu.Lock()
  defer s.mu.Unlock() result := make(map[string]int, len(s.counters))
  for k, v := range s.counters { result[k] = v
  }
  return result
}

// snapshot 现在是一个拷贝
snapshot := stats.Snapshot()

2.4.3、 结构体定义

1、 通常情况下,初始化结构体应该始终指定字段名。

Go 复制代码
k := User{
    FirstName: "John",
    LastName: "Doe",
    Admin: true,
}

2、 但是初始化具有字段名的结构体时,除非提供有意义的上下文,否则忽略值为零的字段。 也就是说让我们自动将这些设置为零值 。( 有助于通过省略该上下文中的默认值来减少阅读的障碍 )

Go 复制代码
反例:

user := User{
  FirstName: "John",
  LastName: "Doe",
  MiddleName: "",
  Admin: false,
}
正例:

user := User{
  FirstName: "John",
  LastName: "Doe",
}

3、 如果在声明中省略了结构的所有字段,请使用 var 声明结构。

Go 复制代码
反例:

user := User{}
正例:

var user User

4、 嵌入结构体中作为成员的结构体,应位于结构体内的成员列表的顶部,并且必须有一个空行将嵌入式成员与常规成员分隔开。

5、 在初始化 Struct 类型的指针实例时,使用 &T{} 代替 new(T),使其与初始化 Struct 类型实例一致。

Go 复制代码
sval := T{Name: "foo"}
sptr := &T{Name: "bar"}

2.5、 接口定义

1、 特别的,如果希望通过接口的方法修改接口实例的实际数据,则必须传递接口实例的指针(将实例指针赋值给接口变量),因为指针指向真正的内存数据。

Go 复制代码
type F interface {
  f()
}

type S1 struct{}

func (s S1) f() {}

type S2 struct{}

func (s *S2) f() {}

// f1.f() 无法修改底层数据。
// f2.f() 可以修改底层数据,给接口变量 f2 赋值时使用的是实例指针。
var f1 F := S1{}
var f2 F := &S2{}

2.6、 函数、方法定义

1、 函数、方法的参数排列顺序遵循以下几点原则(从左到右)。

参数的重要程度与逻辑顺序。

简单类型优先于复杂类型。

尽可能将同种类型的参数放在相邻位置,则只需写一次类型。

2.7、 错误处理

1、 err 总是作为函数返回值列表的最后一个。

2、 如果一个函数 return err,一定要检查它是否为空,判断函数调用是否成功。如果不为空,说明发生了错误,一定要处理它。

3、 不能使用 _ 丢弃任何 return 的 err。若不进行错误处理,要么再次向上游 return err,要么使用 log 记录下来。

4、 尽早 return err,函数中优先进行 return 检测,遇见错误则马上 return err。

5、 错误提示(Error Strings)不需要大写字母开头的单词,即使是句子的首字母也不需要。除非那是个专有名词或者缩写。同时,错误提示也不需要以句号结尾,因为通常在打印完错误提示后还需要跟随别的提示信息。

6、 尽量不要使用 panic,除非你知道你在做什么。只有当实在不可运行的情况下采用 panic,例如:文件无法打开,数据库无法连接导致程序无法正常运行。但是对于可导出的接口不能有 panic,不要抛出 panic 只能在包内采用。建议使用 log.Fatal 来记录错误,这样就可以由 log 来结束程序。

7、 如果我们需要用函数的返回值来初始化某个变量,应该把这个函数调用单独写在一行,例如:

Go 复制代码
反例:

if x, err := f(); err != nil { // error handling return
} else { // use x
}
正例:

x, err := f()
if err != nil { // error handling return
}

8、 采用独立的错误流进行处理。尽可能减少正常逻辑代码的缩进,这有利于提高代码的可读性,便于快速分辨出哪些还是正常逻辑代码,例如:

Go 复制代码
反例:

if err != nil { // error handling
} else { // normal code
}
正例:

if err != nil { // error handling return // or continue, etc.
}

2.8、 其他说明

1、 减少嵌套,代码应通过尽可能先处理错误情况/特殊情况并尽早返回或继续循环来减少嵌套。减少嵌套多个级别的代码的代码量。

Go 复制代码
反例:

for _, v := range data {
  if v.F1 == 1 {
    v = process(v)
    if err := v.Call(); err == nil {
      v.Send()
    } else {
      return err
    }
  } else {
    log.Printf("Invalid v: %v", v)
  }
}
正例:

for _, v := range data {
  if v.F1 != 1 {
    log.Printf("Invalid v: %v", v)
    continue
  }

  v = process(v)
  if err := v.Call(); err != nil {
    return err
  }
  v.Send()
}

2、 减少不必要的else。

Go 复制代码
反例:

var a int
if b {
  a = 100
} else {
  a = 10
}
正例:

a := 10
if b {
  a = 100
}

3、 顶层变量,使用标准的 var 关键字。请勿指定类型,除非它与表达式的类型不同。

Go 复制代码
反例:

var _s string = F()

func F() string { return "A" }
正例:

var _s = F()
// 由于 F 已经明确了返回一个字符串类型,因此我们没有必要显式指定_s 的类型
// 还是那种类型
func F() string { return "A" }

4、 使用 defer 释放资源,比如文件和锁。但是需要注意,无限循环中禁止使用defer。除非在匿名函数中。

5、 Channel 的 size 要么是 1,要么是无缓冲的

三、 注释规范

3.1、 注释风格

1、 统一使用中文注释,中英文之间使用空格分割,严格使用中文标点符号。

2、 注释应当是一个完整的句子,以句号结尾。

3、 句子类型的注释首字母均需大写,短语类型的注释首字母需小写。

4、 注释的单行长度不能超过80个字符。

3.2、 包注释

1、 每个包都应该有一个包注释。放置在包前的一个块注释。对于包含多个文件的包,包注释只需出现在其中的一个文件中即可。包注释应该包含:

包名、简介。

创建者。

创建时间。

2、 对于main包,通常只有一行简短的注释用以说明包的用途,且以项目名称开头。

3、 对于简单的非main包,也可用一行注释概括。

4、 对于一个复杂项目的子包,一般情况下不需要包级别注释,除非是代表某个特定功能的模块。

5、 对于相对功能复杂的非main包,一般都会增加一些使用示例或基本说明,且以Package开头。

6、 对于特别复杂的包说明,一般使用doc.go文件用于编写包的描述,并提供与整个包相关的信息。

3.3、 函数、方法注释

1、 每个函数、方法(结构体或者接口下属的函数称为方法)都应该有注释说明,包括三个方面(顺序严格):

函数、方法名,简要说明。

参数列表,每行一个参数。

返回值,每行一个返回值。

2、 如果一句话不足以说明全部问题,则可换行继续进行更加细致的描述。

3、 若函数或方法为判断类型(返回值主要为bool类型),则注释以:

Go 复制代码
<name> returns true if开头
// HasPrefix returns true if name has any string in given slice as prefix.
func HasPrefix(name string, prefixes []string) bool {...}

3.4、 结构体、接口注释

每个自定义的结构体、接口都应该有注释说明,放在实体定义的前一行,格式为:名称、说明。同时,结构体内的每个成员都要有说明,该说明放在成员变量的后面(注意对齐),例如:

Go 复制代码
// User, 用户实例,定义了用户的基础信息。
type User struct {
    Username string // 用户名
    Email string // 邮箱
}

3.5、 其他说明

1、 当某个部分等待完成时,用 TODO(Your name): 开头的注释来提醒维护人员。

2、 当某个部分存在已知问题进行需要修复或改进时,用 FIXME(Your name): 开头的注释来提醒维护人员。

3、 当需要特别说明某个问题时,可用 NOTE(You name): 开头的注释。

相关推荐
无限大.1 小时前
为什么“云计算“能改变世界?——从本地计算到云端服务
开发语言·云计算·perl
码luffyliu1 小时前
Go 实战: “接口 + 结构体” 模式
后端·go
Lisonseekpan1 小时前
为什么Spring 推荐使用构造器注入而非@Autowired字段注入?
java·后端·spring·log4j
BingoGo1 小时前
PHP 之高级面向对象编程 深入理解设计模式、原则与性能优化
后端·php
草莓熊Lotso1 小时前
Python 流程控制完全指南:条件语句 + 循环语句 + 实战案例(零基础入门)
android·开发语言·人工智能·经验分享·笔记·后端·python
laozhoy11 小时前
深入理解Golang中的锁机制
开发语言·后端·golang
码luffyliu1 小时前
Go 中的深浅拷贝:从城市缓存场景讲透指针与内存操作
后端·go·指针·浅拷贝·深拷贝
雾岛听蓝1 小时前
C++ 模板初阶
开发语言·c++
小杰帅气2 小时前
智能指针喵喵喵
开发语言·c++·算法