大家好,我是极客老墨。

Go作为编程语言中的后起之秀,在语言规范上吸收了诸多语言的优点,并形成了自身独特的语言风格。本文探讨一下Go语言的代码风格。

main方法

main 方法同样是go程序的入门函数,但是其定义非常简单,比如上一篇中的:

1func main() {
2	fmt.Println("Hello, world!")
3}

直接用 func 关键字定义一个名为 main 的方法即可,没有任何参数。

那么,如何获取传递给 main 的参数呢,可以使用 os.Args

1func main() {
2	fmt.Println("hello go!")
3	for i, arg := range os.Args {
4		fmt.Println(i, "=", arg)
5	}
6}

os.Args 获取命令行传递的参数,第一个始终为执行程序的名称,一个示例执行结果如下:

1$ go run main_func.go a b
2hello go!
30 = /var/folders/8b/y3pklwbs1wj_cq7hm_yjwgpr0000gn/T/go-build3458283994/b001/exe/main_func
41 = a
52 = b

当然,通常情况不会使用 os.Args 来解析命令行参数,而是使用 go 标准库的 flag 包。

go的关键字

go的内置关键字有25个:

1break        default      func         interface    select
2case         defer        go           map          struct
3chan         else         goto         package      switch
4const        fallthrough  if           range        type
5continue     for          import       return       var

这25个关键字不能作为标识符使用。

标识符命名

Go中的标识符同java一样,必须由字母、数字和下划线"_“组成,而且第一个不能为数字:

1func main() {
2	_a := 1
3	fmt.Println(_a)
4	b := 2
5	fmt.Println(b)
6	// 3c := 3 // 不能编译
7	你好 := 5
8	println(你好)
9}

有意思的是,除了25个关键字之外的合法标识符,只要不存在冲突,都可以作为标识符,比如包名也可以作为标识符:

1fmt := 4
2println(fmt)
3// fmt.println("a") // 不能编译

上边的代码定义了名为 fmt 的变量标识符,这是合法的,但是后续代码就不能只用 fmt 包了,而标识符声明之前的代码不受影响。

注释

go中有两种注释,比java少一种:

  • 单行注释: 使用 // 开头,注释一行
  • 多行注释:使用 /* ... */ 形式,可以用在行内注释

这两种注释与java相同,但是go中没有文档注释 /** ... */,而文档注释可以通过上边两种即可,而且注释中的关键信息如方法名称、类型名称等 go doc 工具会自动识别(goland中会高亮并可链接):

1// sayHello is a function to say hello with the giving name.
2func sayHello(name string) {
3	fmt.Println("hello, ", name)
4}

未使用的变量和包

go是不允许导入了包不使用,或者声明了变量而未使用的,这些都会造成编译失败。

1func main() {
2	name, err := getName() // 声明了变量err却不使用,编译错误
3	fmt.Println(name)
4}
5
6func getName() (string, error) {
7	return "huzhou", nil
8}

如果不需要使用变量,可以使用下划线 _ 来忽略这个标识符,这被称为 匿名变量,上边的错误代码修改为即可编译:

1name, _ := getName()

同样的,如果导入了包却不使用,也无法通过编译。有时候,我们需要导入包来做一些初始化,比如注册MySQL驱动,但是代码中又不使用,这时就需要使用 _ 让包导入却可以通过编译。比如,gorm框架中的导入mysql包的代码如下:

1_ "github.com/go-sql-driver/mysql"

这样成功导入了mysql驱动包,并可以执行 init 代码来注册驱动了:

1func init() {
2	sql.Register("mysql", &MySQLDriver{})
3}

方法和变量的可见性

Go是通过标识符首字母大小写来控制可见性的,这点与java不同(publicprivateprotected、包访问权限default等)。标识符首字母大写的变量、常量、类型、方法等可以被外部其他模块访问,而首字母小写则意味着这些标识符只能内部私有,外部不可见。

比如,我定义了一个 user 模块:

1var Name = "huzhou"
2
3func getName() string {
4	return Name
5}
6
7func SayHello(name string) {
8	fmt.Println("hello, ", name)
9}

其中,Name 变量、SayHello 方法由于首字母大写,所以可以被外部其他模块访问,而 getName 只能在 user 模块内部访问:

 1func main() {
 2	name := user.Name
 3	fmt.Println(name)
 4
 5    // 不能访问,因为是包内部可见性
 6	// name = user.getName()
 7	// fmt.Println(name)
 8
 9	user.SayHello("huzhou")
10}

不支持方法重载

Go中没有方法重载,重名的方法名称会编译失败。比如下边的代码:

 1var data = make(map[int64]string)
 2
 3func init() {
 4	data[1] = "zhangsan"
 5	data[2] = "lisi"
 6	data[3] = "huzhou"
 7}
 8
 9func GetName(id int64) string {
10	return data[id]
11}

原有的 GetName 方法工作正常,现在如果想要增加一个 age 参数,或者返回值增加一个 error,我们肯定不希望在原来的方法上直接修改,这样会导致素有调用的代码都需要修改。我们希望重载两个方法,结果编译失败:

1func GetName(id int64) (string, error) {
2}
3
4func GetName(id int64, age int8) {
5}

新增的这两个方法都会由于方法重名了而导致编译失败,正确的做法是换一个方法名称。

Go中没有三元运算符

可能让初学者一开始很不习惯的是,Go中没有三元运算符,尤其是对已有其他语言经验的开发者而言。究其原因,Go官方认为,三元运算符会给开发者带来阅读障碍,他们认为许多开发者都存在三元运算符乱用的问题,比如在三元运算符中进行逻辑计算、三元运算符嵌套,甚至偷懒使用非常复杂的三元运算符替代 if...else... 语句,比如下边这样:

1isOdd(a) ? a + b : a * b;
2isOdd(a) ? (a > 10 ? (a < 20 ? a * 2 : a * a) : a) : a / 2;

尽管三元运算符可以减少代码量,但却让代码难以阅读,所以 Go 推荐使用 if 语句,而不提供三元运算符。

 1func main() {
 2    ...
 3	// 编译错误:go中没有三元运算符
 4	// user.Age = validAge(age) ? age: -1
 5	// 使用if语句代替三元运算符
 6	if validAge(age) {
 7		user.Age = age
 8	} else {
 9		user.Age = -1
10	}
11    ...
12}

当然,我们可以定义一个工具方法来实现三元运算效果:

1func Choose(b bool, v1 any, v2 any) any {
2	if b {
3		return v1
4	}
5	return v2
6}

使用时使用类型断言即可:

1ret := Choose(validAge(age), age, -1)
2user.Age = ret.(int) // 类型断言
3fmt.Printf("%v\n", user)

Go 1.18提供了泛型特性,我们也可以定义一个泛型方法来实现,这样就不需要断言了:

1func Choose1[T any](b bool, v1 T, v2 T) T {
2	if b {
3		return v1
4	}
5	return v2
6}

其他代码约束

分号的使用

Go中一行代码的末尾不需要分号 ;,写上分号尽管编译通过,但是显得多余。但是如果一行代码中有多个执行语句,此时需要分号隔开:

1func main() {
2	fmt.Println("Redundant semicolon"); // 分号是多余的
3	defer func() {
4		if err := recover(); err != nil { // `if` 中包括两个语句,需要 `;` 隔开
5			fmt.Println("recovered")
6		}
7	}()
8}

条件判断不需要括号

if 语句判断条件是,不需要 ()

1func main() {
2	rand.Seed(time.Now().UnixNano())
3	a := rand.Intn(100)
4	 if (a%2 == 0) { // 多余的小括号
5		fmt.Println(a, " is an even number")
6	} else {
7		fmt.Println(a, " is an odd number")
8	}
9}

大括号不能换行

Go中,大括号是不能另起一行的,这会造成编译失败。比如下边的代码会编译失败:

1func main()
2{
3}

使用 golangci-lint 保证代码质量

前面讲了这么多代码规范,但是靠人工检查很容易遗漏。老墨强烈推荐使用 golangci-lint 这个工具,它集成了 50+ 个 linter,可以自动检查代码质量问题。

什么是 golangci-lint

golangci-lint 是 Go 社区最流行的代码检查工具。虽然现代 IDE(如 GoLand、VS Code)都内置了基础的代码检查功能,但 golangci-lint 提供了更强大、更全面的检查能力。

为什么需要单独安装?

  • IDE 内置的检查通常只包含基础规则(如 govet、gofmt)
  • golangci-lint 集成了 50+ 个专业 linter,覆盖更全面
  • 可以在命令行、CI/CD 中使用,不依赖 IDE
  • 团队可以通过配置文件统一代码检查标准

老墨这里给出完整的安装和使用教程,帮助你在开发、提交、CI/CD 各个环节都能用上它。

它的优势:

  • 快速:并行运行多个 linter,比单独运行快 5 倍
  • 可配置:支持自定义规则和忽略规则
  • 集成友好:支持 CI/CD、IDE 集成
  • 持续更新:社区活跃,持续更新

安装 golangci-lint

macOS/Linux:

1# 使用官方脚本安装
2curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin
3
4# macOS 也可以使用 Homebrew
5brew install golangci-lint
6
7# 验证安装
8golangci-lint --version

Windows:

1# 使用 Scoop
2scoop install golangci-lint
3
4# 或者下载二进制文件
5# https://github.com/golangci/golangci-lint/releases

基本使用

 1# 检查当前目录
 2golangci-lint run [xxx.go]
 3
 4# 检查指定目录
 5golangci-lint run ./...
 6
 7# 只显示新增的问题
 8golangci-lint run --new
 9
10# 自动修复部分问题
11golangci-lint run --fix

注意,如果没有指定任何目录、Go文件,那么默认需要当前目录是包含模块化的工程。如果你想跑老墨的示例代码,你可以指定文件名,或者使用通配符跑多个文件:

1golangci-lint run *.go --new
2golangci-lint run 01-basic.go --new

配置文件

在项目根目录创建 .golangci.yml 配置文件:

 1# .golangci.yml
 2run:
 3  # 超时时间
 4  timeout: 5m
 5
 6  # 要检查的目录
 7  tests: true
 8
 9  # 跳过的目录
10  skip-dirs:
11    - vendor
12    - third_party
13
14# 启用的 linters
15linters:
16  enable:
17    - gofmt # 检查代码格式
18    - goimports # 检查 import 顺序
19    - govet # 官方静态分析工具
20    - errcheck # 检查未处理的错误
21    - staticcheck # 静态分析
22    - unused # 检查未使用的代码
23    - gosimple # 简化代码建议
24    - ineffassign # 检查无效赋值
25    - misspell # 检查拼写错误
26    - gocritic # 代码审查建议
27    - revive # 替代 golint
28    - stylecheck # 代码风格检查
29    - unparam # 检查未使用的参数
30    - unconvert # 检查不必要的类型转换
31    - prealloc # 检查切片预分配
32    - nolintlint # 检查 nolint 指令
33
34# linters 配置
35linters-settings:
36  # errcheck 配置
37  errcheck:
38    # 检查类型断言
39    check-type-assertions: true
40    # 检查空白标识符
41    check-blank: true
42
43  # govet 配置
44  govet:
45    # 检查 shadow 变量
46    check-shadowing: true
47
48  # gofmt 配置
49  gofmt:
50    # 简化代码
51    simplify: true
52
53  # revive 配置
54  revive:
55    rules:
56      - name: exported
57        severity: warning
58        disabled: false
59      - name: var-naming
60        severity: warning
61        disabled: false
62
63# 问题配置
64issues:
65  # 排除某些规则
66  exclude-rules:
67    # 排除测试文件的某些检查
68    - path: _test\.go
69      linters:
70        - errcheck
71        - gosec
72
73    # 排除 main 函数的某些检查
74    - path: cmd/
75      linters:
76        - gochecknoinits
77
78  # 最多显示的问题数
79  max-issues-per-linter: 50
80  max-same-issues: 3

常见问题修复

1. 未使用的变量

 1// ❌ 错误:声明了但未使用
 2func main() {
 3    name := "hankmo"  // golangci-lint: unused variable
 4}
 5
 6// ✅ 正确:使用变量或删除
 7func main() {
 8    name := "hankmo"
 9    fmt.Println(name)
10}

2. 未处理的错误

 1// ❌ 错误:忽略错误
 2func main() {
 3    file, _ := os.Open("test.txt")  // golangci-lint: error not checked
 4    defer file.Close()
 5}
 6
 7// ✅ 正确:处理错误
 8func main() {
 9    file, err := os.Open("test.txt")
10    if err != nil {
11        log.Fatal(err)
12    }
13    defer file.Close()
14}

3. 导入未使用的包

1// ❌ 错误:导入了但未使用
2import (
3    "fmt"
4    "os"  // golangci-lint: imported but not used
5)
6
7// ✅ 正确:删除未使用的导入
8import "fmt"

IDE 集成

VS Code

安装 Go 扩展后,在 settings.json 中配置:

1{
2  "go.lintTool": "golangci-lint",
3  "go.lintFlags": ["--fast"],
4  "go.lintOnSave": "package"
5}

GoLand

  1. 打开 Settings → Tools → File Watchers
  2. 点击 + 添加 golangci-lint
  3. 配置命令:golangci-lint run --fix $FilePath$

CI/CD 集成

GitHub Actions

 1name: golangci-lint
 2on:
 3  push:
 4    branches:
 5      - main
 6  pull_request:
 7
 8jobs:
 9  golangci:
10    name: lint
11    runs-on: ubuntu-latest
12    steps:
13      - uses: actions/checkout@v3
14      - uses: actions/setup-go@v4
15        with:
16          go-version: "1.23"
17      - name: golangci-lint
18        uses: golangci/golangci-lint-action@v3
19        with:
20          version: latest

老墨的实战建议

  1. 从宽松开始:刚开始不要启用所有 linter,先从基础的开始
  2. 逐步严格:随着团队熟悉,逐步增加检查规则
  3. 团队统一:将配置文件提交到代码库,保证团队使用相同规则
  4. CI 必备:在 CI/CD 中集成 golangci-lint,防止不规范代码合并
  5. 定期更新:定期更新 golangci-lint 版本,获取最新规则

老墨总结

本文介绍了 Go 的代码风格和规范,老墨总结 5 个关键点:

  1. 简洁至上:Go 的设计哲学是简洁,没有三元运算符、没有方法重载,强制统一风格
  2. 可见性控制:通过首字母大小写控制可见性,比 Java 的 public/private 更简单直观
  3. 严格检查:未使用的变量和包会导致编译失败,强制开发者写干净的代码
  4. 工具辅助:使用 gofmt、goimports、golangci-lint 等工具自动化检查和格式化
  5. 团队协作:统一的代码风格让团队协作更顺畅,代码审查更高效

实战建议

1. 养成好习惯

  • 每次提交前运行 golangci-lint:确保代码质量
  • 使用 gofmt 格式化代码:保持代码风格一致
  • IDE 配置自动格式化:保存时自动格式化和导入整理

2. 团队规范

  • 制定团队代码规范文档:明确团队的代码风格
  • Code Review 检查清单:包含代码风格检查项
  • 新人培训:让新人了解团队的代码规范

3. 工具链配置

 1# 创建 Makefile 简化命令
 2.PHONY: lint fmt test
 3
 4# 代码检查
 5lint:
 6	golangci-lint run ./...
 7
 8# 代码格式化
 9fmt:
10	gofmt -s -w .
11	goimports -w .
12
13# 运行测试
14test:
15	go test -v -race -coverprofile=coverage.out ./...
16
17# 提交前检查
18pre-commit: fmt lint test
19	@echo "✅ All checks passed!"

4. 常见误区

  • 不要过度追求完美:代码规范是为了提高可读性,不是为了追求 100% 完美
  • 不要盲目禁用规则:如果 linter 报错,先思考是否真的有问题
  • 不要忽视警告:警告往往是潜在 bug 的信号
  • 不要在代码中滥用 nolint:只在确实需要时使用

练习题

练习题 1:代码风格检查(⭐)

检查以下代码的风格问题,并修复:

1package main
2import "fmt"
3func main()
4{
5    Name := "huzhou"
6    age := 18
7    fmt.Println(Name)
8}

练习题 2:安装和使用 golangci-lint(⭐⭐)

  1. 在你的系统上安装 golangci-lint
  2. 创建一个简单的 Go 项目
  3. 运行 golangci-lint run 检查代码
  4. 修复所有报告的问题

练习题 3:配置 golangci-lint(⭐⭐⭐)

  1. 创建 .golangci.yml 配置文件
  2. 启用至少 10 个 linter
  3. 配置忽略测试文件的某些检查
  4. 在项目中运行并修复所有问题

练习题 4:实现三元运算符(⭐⭐)

使用泛型实现一个通用的三元运算符函数,并编写测试用例:

 1func Ternary[T any](condition bool, trueVal, falseVal T) T {
 2    // 你的实现
 3}
 4
 5// 测试用例
 6func TestTernary(t *testing.T) {
 7    // 测试整数
 8    result := Ternary(true, 1, 2)
 9    // 测试字符串
10    // 测试结构体
11}

练习题 5:代码审查(⭐⭐⭐)

审查以下代码,找出所有不符合 Go 规范的地方:

 1package main
 2
 3import (
 4    "fmt"
 5    "os"
 6    "strings"
 7)
 8
 9func GetUserName(id int64) string {
10    users := map[int64]string{
11        1: "zhangsan",
12        2: "lisi",
13    }
14    name, ok := users[id]
15    if ok == true {
16        return name
17    } else {
18        return ""
19    }
20}
21
22func main() {
23    var name string = GetUserName(1)
24    fmt.Println(name)
25}

思考题

  1. 为什么 Go 不支持方法重载? 这样设计有什么好处和坏处?
  2. 为什么 Go 强制要求未使用的变量和包会导致编译失败? 这样设计的初衷是什么?
  3. 你认为 Go 的代码风格设计理念是什么? 与其他语言相比有什么特点?
  4. golangci-lint 集成了 50+ 个 linter,你可以通过 golangci-lint help linters 查看,你认为哪些是最重要的? 为什么?

答案咱们评论区见!


极客老墨,继续折腾!

如果有任何问题,欢迎在评论区留言或关注公众号「极客老墨」交流。

本文示例代码在 go-tutorial-code/02-style


相关阅读