共计 9775 个字符,预计需要花费 25 分钟才能阅读完成。
以后版本: v1.0.20201106
GitHub: shockerli/go-code-guide
命名规定
- 站在调用者的角度,包不是给你本人用的
- 简洁、且见名知义
- 采纳通用、公众熟知的缩写命名。比方
buf
而不是bufio
- 如果缩写的名字会产生歧义,则放弃或换个
文件名
整个利用或包的主入口文件该当是 main.go
,或与利用名称简写雷同。
比方:spiker
包的主入口文件是 spiker.go
,利用的主入口文件是 main.go
包名
-
包名与目录名统一
如果一个目录下同时呈现多个
package
,则编译失败:found packages pkg (a.go) and pb (b.go) in XXX
- 大多数应用命名导入的状况下,不须要重命名
少让调用者去起别名,除非名字太烂
- 全副小写,没有下划线、大写。谬误示例
MyPackage
、my_package
、myPackage
- 不必复数。例如
net/url
,而不是net/urls
- 不必信息量有余的名字。谬误示例
common
、lib
、util
导入包
- 如果程序包名称与导入门路的最初一个元素不匹配,则必须应用导入别名
import (
client "example.com/client-go"
trace "example.com/trace/v2"
)
- 在所有其余状况下,除非导入之间有间接抵触,否则应防止导入别名
import (
"net/http/pprof"
gpprof "github.com/google/pprof"
)
- 如遇重名,请保留规范包而别名自定义或第三方包
- 在非测试文件(
*_test.go
)中,禁止应用.
来简化导入包的对象调用 - 禁止应用相对路径导入(
./subpackage
),所有导入门路必须合乎go get
规范
驼峰命名法
常量、变量、类型、构造体、接口、函数、办法、属性等,全副应用驼峰法 MixedCaps 或 mixedCaps。
下划线结尾的命名更不容许,Go 语言的公私有对立用大小写结尾来辨别。
但有个例外,为了对相干的测试用例进行分组,函数名可能蕴含下划线,如:TestMyFunction_WhatIsBeingTested。
Bad:
const ROLE_NAME = 10
Good:
const RoleName = 10
常量
- 如果是枚举类型的常量,须要先创立相应类型
type Scheme string
const (
Http Scheme = "http"
Https Scheme = "https"
)
- 如果模块的性能较为简单、常量名称容易混同的状况下,为了更好地区分枚举类型,能够应用残缺的前缀
type Symbol string
const (
SymbolAdd Symbol = "+"
SymbolSub Symbol = "-"
)
变量
-
在绝对简略的环境(对象数量少、针对性强)中,能够将一些名称由残缺单词简写为单个字母
user
能够简写为u
userId
能够简写uid
- 若变量类型为
bool
类型,则名称应以Has
、Is
、Can
或Allow
结尾
var isExist bool
var hasConflict bool
var canManage bool
var allowGitHook bool
URL
- URL 命名全副小写
- 用正斜杠
/
表明层级关系 - 应用连字符
-
来进步长门路中名称的可读性 - 不得在 URL 中应用下划线
_
- URL 结尾不应蕴含正斜杠
/
- 文件扩展名不应蕴含在 URL 中
- URL 需见名知意,但不可裸露服务器架构
Bad:
/GetUserInfo
/photos_path
/My-Folder/my-doc/
/user/user-list
Good:
/user/list
/user/operator-logs
函数 / 办法名
- 不要画龙点睛
- 长命名并不会使其更具可读性,一份有用的阐明文档通常比额定的长名更有价值
Bad:
once.DoOrWaitUntilDone(f)
Good:
once.Do(f)
- 在 pkg 包中名为 New 的函数会返回一个 pkg.Pkg 类型的值
q := list.New() // q is a *list.List
- 当 pkg 包中某个函数的返回值类型为 pkg.Pkg(或 *pkg.Pkg)时,函数名应省略类型名
start := time.Now() // start is a time.Time
t, err := time.Parse(time.Kitchen, "6:06PM") // t is a time.Time
- 当函数返回的值类型为 pkg.T 且 T 不为 Pkg 时,函数名应蕴含 T 以便让用户代码更易了解
ticker := time.NewTicker(d) // ticker is a *time.Ticker
timer := time.NewTimer(d) // timer is a *time.Timer
- 获取器 / 设置器
Go 并不对获取器(getter)和设置器(setter)提供主动反对。针对某个变量或字段,获取器名字无需携带
Get
,设置器名字以Set
结尾。若你有个名为 owner(小写,未导出)的字段,其获取器该当名为 Owner(大写,可导出)而非 GetOwner。
Bad:
owner := obj.GetOwner()
if owner != user {obj.SettingOwner(user)
}
Good:
owner := obj.Owner()
if owner != user {obj.SetOwner(user)
}
- 若函数或办法为判断类型(返回值次要为 bool 类型),则名称应以
Has
、Is
、Can
或Allow
等判断性动词结尾
func HasPrefix(name string, prefixes []string) bool {...}
func IsEntry(name string, entries []string) bool {...}
func CanManage(name string) bool {...}
func AllowGitHook() bool { ...}
接口名
依照约定,只蕴含一个办法的接口该当以该办法的名称加上 -er 后缀来命名,如 Reader、Writer、Formatter/CloseNotifier 等。
名词用于接口名,动词用于接口的办法名。
type Reader interface {Read(p []byte) (n int, err error)
}
Error
Error
类型的命名以Error
结尾
type ParseError struct {Line, Col int}
Error
类型的变量,以Err
结尾
var ErrBadAction = errors.New("somepkg: a bad action was performed")
- 返回类型为
Error
的变量缩写采纳err
func foo() {res, err := somepkgAction()
if err != nil {if err == somepkg.ErrBadAction {}
if pe, ok := err.(*somepkg.ParseError); ok {
line, col := pe.Line, pe.Col
// ....
}
}
}
其余
包内容的名字不能够包名结尾,因为无需反复包名
http
包提供的 HTTP 服务名为 http.Server
,而非 HTTPServer
。用户代码通过 http.Server
援用该类型,因而没有歧义。
不同包中的类型名能够雷同,因为客户端可通过包名辨别它们
例如,规范库中含有多个名为 Reader 的类型,包含 jpeg.Reader
、bufio.Reader
和 csv.Reader
。每个包名搭配 Reader 都是个不错的类型名。
名词缩写表
缩写名 | 阐明 |
---|---|
ctx |
Context 或相干,比方 gin.Context |
分号
Go 其实也是用分号(;
)来完结语句,但 Go 与 JavaScript 一样不倡议给繁多语句开端加分号,因为编译器会主动加分号。
像如下语句是齐全能够的:
go func() { for { dst <- <-src} }()
通常 Go 程序只在诸如 for 循环子句这样的中央应用分号,以此来将初始化器、条件及增量元素离开。如果你在一行中写多个语句,也须要用分号隔开。
if err := f(); err != nil {g()
}
也是因为这个起因,函数或管制语句的左大括号绝不能放在下一行。
if i < f() // 报错
{ // 报错
g()}
圆括号
控制结构(if、for 和 switch)不须要圆括号,语法上就不须要
文档
README
、我的项目文档、接口文档等,中文 文档的排版参考:中文文案排版指北
正文
- 所有导出对象必须正文阐明其用处,非导出对象依据状况进行正文
- 如果对象可数且无明确指定数量的状况下,一律应用复数模式和个别进行时形容,否则应用复数模式
- 包、函数、办法和类型的正文阐明都是一个残缺的句子
- 句子类型的正文首字母均需大写,短语类型的正文首字母需小写
- 正文的单行长度不能超过 80 个字符,超过请强制换行
- 可导出对象的正文,必须以对象的名称作为结尾
// FileInfo is the interface that describes a file and is returned by Stat and Lstat
type FileInfo interface { ...
// HasPrefix returns true if name has any string in given slice as prefix
func HasPrefix(name string, prefixes []string) bool {...
单行正文 & 多行正文
- 两种正文格调,单行正文
//
,多行正文/* ... */
- 多行正文仅用于包级别的文档正文,除此之外请用单行正文。包正文个别搁置到
doc.go
文件,且该文件仅蕴含文档正文内容 - 单行正文符号与内容之间,请用 一个空格 隔开
Bad:
//Comments
Good:
// Comments
GoLand 可设置主动格式化:
Preferences > Editor > Code Style > Go > Other
勾选上 Add leading space to comments
包正文
- 包级别的正文就是对包的介绍,只需在同个包的任一源文件中阐明即可无效
- 对于
main
包,个别只有一行简短的正文用以阐明包的用处,且以项目名称结尾
// Write project description
package main
- 对于一个简单我的项目的子包,个别状况下不须要包级别正文,除非是代表某个特定性能的模块
- 对于简略的非
main
包,也可用一行正文概括 - 对于绝对性能简单的非
main
包,个别都会减少一些应用示例或根本阐明,且以Package <name>
结尾
/*
Package http provides HTTP client and server implementations.
...
*/
package http
- 特地简单的包阐明,可独自创立
doc.go
文件来加以阐明
函数与办法
- 如果一句话不足以阐明全副问题,则可换行持续进行更加粗疏的形容
// Copy copies file from source to target path.
// It returns false and error when error occurs in underlying function calls.
- 若函数或办法为判断类型(返回值次要为
bool
类型),则以<name> returns true if
结尾
// HasPrefix returns true if name has any string in given slice as prefix.
func HasPrefix(name string, prefixes []string) bool {...
构造、接口及其它类型
- 类型的定义个别都以复数模式形容:
// Request represents a request to run a command.
type Request struct {...
- 如果为接口,则个别以以下模式形容:
// FileInfo is the interface that describes a file and is returned by Stat and Lstat.
type FileInfo interface {...
- 如果构造体属性较多,需对属性增加正文
// Var variable for expression
type Var struct {
Key string `json:"key"` // variable key
Value interface{} `json:"value"` // value
Desc string `json:"desc"` // variable description
}
其余阐明
- 当某个局部期待实现时,可用
TODO:
结尾的正文来揭示保护人员。 - 当某个局部存在已知问题进行须要修复或改良时,可用
FIXME:
结尾的正文来揭示保护人员。 - 当须要特地阐明某个问题时,可用
NOTE:
结尾的正文:
// NOTE: os.Chmod and os.Chtimes don't recognize symbolic link,
// which will lead "no such file or directory" error.
return os.Symlink(target, dest)
格式化
咱们没有太多可选的余地,因为 Go 曾经标准好了,在 Go 世界没有此类和平。
缩进
缩进对立采纳 4 个空格,禁用制表符。
EditorConfig 设置:
[{Makefile,go.mod,go.sum,*.go}]
indent_style = tab
indent_size = 4
或 GoLand 设置:
Preferences > Editor > Code Style > Go > Tabs and Indents
空行
- 适当减少空行以放弃代码段落清晰
其余
函数分组与程序
- 函数应按粗略的调用程序排序
- 同一文件中的函数应按接收者分组
- 导出的函数应先呈现在文件中,放在
struct
、const
和var
定义的前面。 - 在定义类型之后,但在接收者的其余办法之前,可能会呈现一个
newXYZ()
/NewXYZ()
。 - 因为函数是按接收者分组的,因而一般工具函数应在文件开端呈现。
- 因而,个别一个
struct
及相干办法组织为一个文件。
Bad:
func (s *something) Cost() {return calcCost(s.weights)
}
type something struct{...}
func calcCost(n int[]) int {...}
func (s *something) Stop() {...}
func newSomething() *something {return &something{}
}
Good:
type something struct{...}
func newSomething() *something {return &something{}
}
func (s *something) Cost() {return calcCost(s.weights)
}
func (s *something) Stop() {...}
func calcCost(n int[]) int {...}
缩小嵌套
Bad:
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)
}
}
Good:
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()}
不必要的 else
- 少数时候,咱们能够把 else 分支里的代码提取为初始化。
Bad:
var a int
if b {a = 100} else {a = 10}
Good:
a := 10
if b {a = 100}
全局变量申明
- 全局变量,必须应用
var
关键字 - 请勿指定类型,除非它与表达式的类型不同
Bad:
var a string = "abc"
var s string = F()
func F() string { return "A"}
Good:
var a = "abc"
// 因为 F() 曾经明确了返回一个字符串类型,因而咱们没有必要显式指定 s 的类型
var s = F()
func F() string { return "A"}
- 如果表达式的类型与所需的类型不齐全匹配,请指定类型
type myError struct{}
func (myError) Error() string { return "error"}
func F() myError { return myError{} }
var err error = F()
// F() 返回一个 myError 类型的实例,然而咱们要 error 类型
局部变量申明
- 如果将变量明确设置为某个值,则应应用短变量申明模式(
:=
)
Bad:
var s string = "abc"
Good:
s := "abc"
- 如果变量专用于援用,则应用
var
关键字更适合
func s() {
var s string
f(&s)
}
- 如果变量是返回值,则定义在函数返回类型中
func f(list []int) (filtered []int) {
for _, v := range list {
if v > 10 {filtered = append(filtered, v)
}
}
return
}
import 包导入分组与排序
- 同一文件,如果导入多个包,对其进行分组
- 规范包 、 第三方包 、 自定义包,别离分组、空行分隔、排序
Bad:
import "a"
import "golang.org/x/sys"
import "runtime"
import "github.com/gin-gonic/gin"
import "b"
import "fmt"
Good:
import (
"fmt"
"runtime"
"a"
"b"
"github.com/gin-gonic/gin"
"golang.org/x/sys"
)
GoLand 设置如下:
类似的申明进行分组
对于 var
、const
、type
等申明语句:
- 将类似的申明放在一个组内
Bad:
const a = 1
const b = 2
var a = 1
var b = 2
type Area float64
type Volume float64
Good:
const (
a = 1
b = 2
)
var (
a = 1
b = 2
)
type (
Area float64
Volume float64
)
- 仅将相干的申明放在一组,不要将不相干的申明放在一组
Bad:
type Operation int
const (
Add Operation = iota + 1
Subtract
Multiply
RoleName = "Role Name"
)
Good:
type Operation int
const (
Add Operation = iota + 1
Subtract
Multiply
)
const RoleName = "Role Name"
- 分组应用的地位没有限度,函数内也可应用分组
Bad:
func f() string {var red = color.New(0xff0000)
var green = color.New(0x00ff00)
var blue = color.New(0x0000ff)
// ...
}
Good:
func f() string {
var (red = color.New(0xff0000)
green = color.New(0x00ff00)
blue = color.New(0x0000ff)
)
// ...
}
函数 / 办法的参数 / 返回类型程序
- 简略类型优先于简单类型
Bad:
func F(u User, n int) {}
Good:
func F(n int, u User) {}
- 尽可能将同种类型的参数放在相邻地位,则只需写一次类型
Bad:
func F(a int, c string, b int) {}
Good:
func F(a, b int, c string) {}
error
永远在最初一个返回类型
Bad:
func F() (error, int) {}
Good:
func F() (int, error) {}
构造体属性程序
咱们先看下示例:
构造体 A – 定义:
struct {
a string
c string
b bool
d bool
}
构造体 A – 大小为 40,内存布局图:
比照,构造体 B – 定义:
struct {
a string
b bool
c string
d bool
}
构造体 B – 大小为 48,内存布局图:
咱们发现,构造体的属性程序不同,占用的内存大小和布局是齐全不同的。
那咱们因而 约定:将雷同类型的属性尽量搁置在一起。即,举荐构造体 A 中的定义程序。
构造体中的嵌入
嵌入式类型(例如 mutex)应位于构造体内的字段列表的顶部,并且必须有一个空行将嵌入式字段与惯例字段分隔开。
Bad:
type Client struct {
version int
http.Client
}
Good:
type Client struct {
http.Client
version int
}
初始化构造体时必须指定字段名
必须 在初始化构造体时指定字段名,否则相干工具和 Review 都不给过。如果不指定,会对代码重构造成不可预期的结果。
Bad:
k := User{"John", "Doe", true}
Good:
k := User{
FirstName: "John",
LastName: "Doe",
Admin: true,
}
惟一例外:如果有 3 个或更少的字段,则能够在测试表中省略字段名
tests := []struct{}{
op Operation
want string
}{{Add, "add"},
{Subtract, "subtract"},
}
放大变量作用域
- 如果有可能,尽量放大变量作用范畴,除非它与缩小嵌套的规定抵触
Bad:
err := ioutil.WriteFile(name, data, 0644)
if err != nil {return err}
Good:
if err := ioutil.WriteFile(name, data, 0644); err != nil {return err}
- 如果须要在 if 之外应用函数调用的后果,则不应尝试放大范畴
Bad:
if data, err := ioutil.ReadFile(name); err == nil {err = cfg.Decode(data)
if err != nil {return err}
fmt.Println(cfg)
return nil
} else {return err}
Good:
data, err := ioutil.ReadFile(name)
if err != nil {return err}
if err := cfg.Decode(data); err != nil {return err}
fmt.Println(cfg)
return nil
Error 信息不应大写或标点符号完结
Bad:
fmt.Errorf("Something bad.")
Good:
fmt.Errorf("something bad")
Error 形容信息是须要被包裹或援用形容的,那么上面的代码将通知咱们为何不应如此:
log.Printf("Reading %s: %v", filename, err)
slice
nil
是一个无效长度为 0 的 slice
- 零值切片可立刻应用,无需调用 make 创立
Bad:
nums := []int{}
// or, nums := make([]int, 0)
if add1 {nums = append(nums, 1)
}
Good:
var nums []int
if add1 {nums = append(nums, 1)
}
- 要查看切片是否为空,请始终应用
len(s) == 0
,不要查看nil
Bad:
func isEmpty(s []string) bool {return s == nil}
Good:
func isEmpty(s []string) bool {return len(s) == 0
}
- 对于须要序列化的切片,则必须应用 make 初始化
Bad:
var v []int
s, _ := json.Marshal(v)
println(string(s))
// output: null
Good:
v := make([]int, 0)
s, _ := json.Marshal(v)
println(string(s))
// output: []
参考资料
- Package names – Go Blog
- Style guideline for Go packages
- Organizing Go code – Go Blog
- How to Write Go Code – Go Blog
- Effective Go
- Go Code Convention
- Go Wiki – Errors
- Uber Go Style Guide
感谢您的浏览,感觉内容不错,点个赞吧 ????
原文地址: https://shockerli.net/post/go-code-guide-style