学习一门编程语言我通常会从下面这些内容入手,本次学习 Golang 语法也是。在此形成一份语法摘要。

  1. 基础:变量、数据类型、运算符、注释
  2. 控制:条件、循环、分支
  3. 数据结构:数组、列表、元组、集合、字典
  4. 函数:定义、参数、返回值、嵌套
  5. 面向对象:类、对象、继承、多态、封装
  6. 其他:异常、模块化、新特性

注释

单行注释 //

// 这是一个行注释
x := 42  // 行尾注释

多行注释

/*
这是一个多行注释
可以跨越多行
*/

官方强烈推荐使用 // 行注释,即使是多行也用多个 //


打印(Print)

在 Go 语言中,没有内置的 print 关键字,但标准库提供了多个用于输出的函数。

最常用:fmt 包(格式化输出)

  1. fmt.Println。输出到 os.Stdout,带换行的打印。自动在参数间加空格,末尾加换行符 \n。支持任意类型(调用其 String() 方法或默认格式)

    fmt.Println("Hello", "World")        // 输出: Hello World\n
    fmt.Println(42, true, []int{1,2,3})  // 输出: 42 true [1 2 3]\n
    
  2. fmt.Print。输出到 os.Stdout,不带换行,参数间不自动加空格,需手动控制格式。

    fmt.Print("Hello")
    fmt.Print("World")  // 输出: HelloWorld(无空格、无换行)
    
  3. fmt.Printf。输出到 os.Stdout,格式化输出(类似 C 的 printf

    name := "Alice"
    age := 30
    fmt.Printf("Name: %s, Age: %d\n", name, age)
    // 输出: Name: Alice, Age: 30
    

    常用格式动词:

    动词 说明 示例
    %v 默认格式 fmt.Printf("%v", x)
    %+v 结构体显示字段名 fmt.Printf("%+v", user)
    %#v Go 语法表示(可复制回代码) fmt.Printf("%#v", []int{1,2})[]int{1, 2}
    %T 类型 fmt.Printf("%T", x)int
    %s 字符串 "hello"
    %d 十进制整数 42
    %x 十六进制 2a
    %f 浮点数 3.141592
    %t 布尔值 true

    推荐:优先用 %v,结构体用 %+v,调试用 %#v

其他打印方式

  1. printlnprint(内置函数,输出到 stderr,仅用于调试!官方文档明确要求 “不要在生产代码中使用!”)

    print("hello")   // 输出到 stderr,无空格、无换行
    println("world") // 输出 + 换行
    
  2. log 包(带时间戳的日志输出),输出到 stderr,适合写日志,不适合普通打印

    import "log"
    
    log.Println("This is a log message")
    log.Printf("Error: %v", err)
    
  3. 直接写 os.Stdout / os.Stderr。底层操作,一般不需要,fmt 内部就是基于这些实现的。

    import "os"
    
    os.Stdout.Write([]byte("hello\n"))
    

记住:在 Go 中,fmt 是你打印输出的唯一朋友。


变量(Variables)

变量声明与使用可以分成两行来写。

var a int
a = 10

因为 Golang 是支持类型推断的,我一般习惯在同一行完成声明和初始化。甚至如果是在函数内部进行变量声明,连 var关键字都可以省去,但要使用海豹运算符。

var b string = "Hello"  // 完整的声明 + 初始化
var c = 3.14  // 借助类型推断,可以这样简写

func main() {
    d := "World"  // 简短变量声明,只能在函数内部使用
}

多变量声明

var e, f int = 5, 10  // 多个变量的完整声明
g, h := 15, "Go语言"  // 多个变量的简短声明
var (                 // 多变量声明分组
    m = 100
    n = "分组声明"
    o = false
)

当一个变量被声明但未显式初始化时,Go 会自动将其赋值为该类型的零值。不同数据类型的零值 (Zero Value) 是不一样的。

// 零值(Zero Value)
var i int     // 0
var j float64 // 0.0
var k bool    // false
var l string  // ""

Golang 在变量的使用上还是比较常规的。


常量(Constants)

// 全局常量声明
const Pi = 3.1415926
const Version = "1.0.0"
// 常量分组声明
const (
    StatusOK    = 200
    StatusError = 500
    StatusNotFound = 404
)

常量计数器 (iota)

// 常量计数器(iota)
const (
    Monday = iota  // iota 从0开始,每行递增1
    Tuesday
    Wednesday
    Thursday
    Friday
    Saturday
    Sunday
)
// 常量表达式
const (
    // 位运算示例
    Bit0 = 1 << iota // 1 << 0 = 1
    Bit1             // 1 << 1 = 2
    Bit2             // 1 << 2 = 4
    Bit3             // 1 << 3 = 8
)

类型化常量

const MaxAge int = 120

无类型常量(可以隐式转换)

const Count = 100
var num1 int = Count
var num2 float64 = Count

常量不能使用简短变量声明语法 :=


数据类型(Data Types)

Go 语言(Golang)的数据类型体系清晰、简洁,分为 基本数据类型复合/引用数据结构。以下是全面整理:

分类 类型
基本类型 bool, int/uint, float, complex, string, byte, rune
复合类型 array, slice, map, struct
引用类型 slice, map, channel, pointer, function(底层是指针)
其他 interface{}, error, chan

其中日常开发中最常用的是基本类型 (int, string, bool, float64) 和数据结构 (slice, map, struct)。

可以使用 fmt.Printf("%T", x) 来查看变量 x 的类型(Type)。

布尔类型 (bool)

取值为 truefalse

var ok bool = true

数值类型

  1. 整数类型 (int),有符号 int 是最常用的整数类型,除非有特殊需求(如节省内存、协议对齐),否则优先用 int

    类型 范围 大小
    int8 -128 ~ 127 1 字节
    int16 -32768 ~ 32767 2 字节
    int32 -2³¹ ~ 2³¹-1 4 字节
    int64 -2⁶³ ~ 2⁶³-1 8 字节
    int 平台相关(32/64位系统) 通常 8 字节(64位机)
  2. 无符号整数 (uint)

    byteuint8 的别名,runeint32 的别名(用于 Unicode 字符)。

    类型 范围 大小
    uint8 0 ~ 255 1 字节(等价于 byte
    uint16 0 ~ 65535 2 字节
    uint32 0 ~ 2³²-1 4 字节
    uint64 0 ~ 2⁶⁴-1 8 字节
    uint 平台相关 int
    uintptr 存储指针地址 平台相关
  3. 浮点数 (float)

    var pi float64 = 3.1415926
    
    类型 精度 大小
    float32 约 7 位十进制 4 字节
    float64 约 15 位十进制 8 字节(默认)
  4. 复数(较少用)

    complex64(实部/虚部为 float32);complex128(实部/虚部为 float64)。

    c := 3 + 4i // 默认 complex128
    

字符串类型 (string)

字符串类型是 UTF-8 编码的只读字节序列。在 Golang 中,字符串是不可变的,修改需创建新字符串。

s := "Hello, 世界"

如果要表示其他语言中的 char 类型,可以使用内置类型 runerune 实际上是 int32 的别名,并且 rune 基于 Unicode 而非单字节。

var r rune = 'A'        // ASCII 字符
var r2 rune = '中'      // 中文字符
var r3 rune = '😊'      // Emoji

🔔 注意:字符字面量用 单引号 ',字符串用双引号 "


运算符

Golang 的运算符设计简洁、明确,不支持运算符重载

类别 运算符 名称 / 含义 说明 示例
算术运算符 + 加法 数值相加或字符串拼接 3 + 25"a" + "b""ab"
- 减法 数值相减 5 - 23
* 乘法 数值相乘 4 * 312
/ 除法 整数除法截断(向零取整),浮点正常除 7 / 237.0 / 23.5
% 取模(余数) 仅用于整数类型 7 % 31
++ 自增 只能作为独立语句,不能用于表达式 i++ ✅;a = i++
-- 自减 同上 i--
比较运算符 == 等于 判断两个值是否相等(类型必须可比较) x == y
!= 不等于 x != y
< 小于 支持数字、字符串(字典序) "a" < "b"true
<= 小于等于
> 大于
>= 大于等于
逻辑运算符 && 逻辑与 短路求值:左为 false 时右不执行 a > 0 && a < 10
` ` 逻辑或
! 逻辑非 取反布尔值 !ok
位运算符 (仅整数) & 按位与 二进制位逐位与 5 & 31101 & 011 = 001
` ` 按位或
^ 按位异或 相同为 0,不同为 1 5 ^ 36
&^ 位清除(AND NOT) 清除左操作数中右操作数为 1 的位 0b1111 &^ 0b10100b0101
<< 左移 左操作数左移右操作数位(高位丢弃) 1 << 38
>> 右移 无符号右移(逻辑右移) 8 >> 22
赋值运算符 = 赋值 基本赋值 x = 10
+= 加后赋值 x += y 等价于 x = x + y
-= 减后赋值
*= 乘后赋值
/= 除后赋值
%= 模后赋值
&=, ` =, ^=, &^=` 位运算赋值
<<=, >>= 移位赋值 x <<= 2
指针/地址运算符 & 取地址 获取变量内存地址 p := &x
* 解引用 通过指针访问值 v := *p
通道操作符 <- 发送 / 接收 用于 channel 通信 ch <- data(发送) data := <-ch(接收)
其他 _ 空标识符 用于丢弃不需要的值(非运算符,但常配合使用) _, err := fmt.Println("hi")

重要限制与注意事项:

  1. 不支持三元运算符,必须用 if-else 实现条件赋值
  2. 不支持运算符重载,所有运算符行为固定,不可自定义
  3. ++-- 是语句,不是表达式,不能写成 a = i++
  4. slice、map、function 不可比较,不能用 ==,但可以和 nil 比较
  5. 字符串支持 + 拼接,但不支持 - 等其他算术运算,"a" + "b" ✅;"ab" - "b"
  6. 位运算仅适用于整数类型。浮点数、字符串等不能进行位运算
  7. 通道 <- 方向由上下文决定,在赋值左边是接收,在右边是发送

运算符优先级 (从高到低,官方简化版):

( )         // 圆括号(最高)
* / % << >> & &^
+ - | ^
== != < <= > >=
&&
||          // 最低

建议多用括号 () 明确意图,避免依赖优先级


条件语句(Conditionals)

Golang 在语法上比较精炼,因此仅有 2 种条件语句:if-elseswitch-case


if-else

土土的基本用法:

num := 10
if num > 0 {
    fmt.Println("num是正数")
}

连续使用:

score := 85
if score >= 90 {
    fmt.Println("优秀")
} else if score >= 80 {
    fmt.Println("良好")
} else if score >= 60 {
    fmt.Println("及格")
} else {
    fmt.Println("不及格")
}

if 语句中的初始化语句:

if age := 18; age >= 18 {
    fmt.Println("已成年")
} else {
    fmt.Println("未成年")
}

注意,age 变量的作用域仅限于 if-else 语句块。


switch-case

土土的基本用法

day := 3

switch day {
case 1:
    fmt.Println("星期一")
case 2:
    fmt.Println("星期二")
case 3:
    fmt.Println("星期三")
case 4:
    fmt.Println("星期四")
case 5:
    fmt.Println("星期五")
case 6, 7:
    fmt.Println("周末")
default:
    fmt.Println("无效的星期")
}

switch 语句中的初始化语句

switch temperature := 25; {
case temperature < 0:
    fmt.Println("寒冷")
case temperature >= 0 && temperature < 20:
    fmt.Println("凉爽")
case temperature >= 20 && temperature < 30:
    fmt.Println("舒适")
default:
    fmt.Println("炎热")
}

switch 语句中的 fallthrough(默认不穿透)

grade := "A"

switch grade {
case "A":
    fmt.Println("优秀")
    fallthrough // 继续执行下一个case
case "B":
    fmt.Println("良好")
case "C":
    fmt.Println("及格")
default:
    fmt.Println("不及格")
}

类型判断 (type switch)

var x interface{}
x = 100

switch v := x.(type) {
case int:
    fmt.Printf("x是int类型,值为%d\n", v)
case string:
    fmt.Printf("x是string类型,值为%s\n", v)
case bool:
    fmt.Printf("x是bool类型,值为%v\n", v)
default:
    fmt.Printf("x是未知类型,值为%v\n", v)
}

语法上依旧平平无奇。


循环(Loop)

Golang 在循环上就更吝啬了,仅仅只有 for-Loop 这一种循环语句。一个标准的 for 循环语法如下(类似C语言):

for i := 0; i < 5; i++ {
    fmt.Printf("i = %d\n", i)
}

其中 i: = 0 叫做初始化,i < 5 叫做循环条件,i++ 叫做更新。这三个部分都是可以省略的。

// for循环(省略初始化)
j := 0
for ; j < 5; j++ {
    fmt.Printf("j = %d\n", j)
}

// for循环(省略初始化和更新)
k := 0
for k < 5 {
    fmt.Printf("k = %d\n", k)
    k++
}

// 无限循环(Go语言没有while循环)
count := 0
for {
    fmt.Printf("循环计数: %d\n", count)
    count++
    if count >= 3 {
        break // 跳出循环
    }
}

for 循环遍历数组

numbers := [5]int{1, 2, 3, 4, 5}
for i := 0; i < len(numbers); i++ {
    fmt.Printf("numbers[%d] = %d\n", i, numbers[i])
}

for range 循环(遍历数组、切片、映射等)

// 普普通通的写法,numbers := [5]int{1, 2, 3, 4, 5}
for index, value := range numbers {
    fmt.Printf("numbers[%d] = %d\n", index, value)
}

// 如果不需要用到索引(index),能用 '_' 来省略索引
for _, value := range numbers {
    fmt.Printf("值 = %d\n", value)
}

// 反过来,如果只关心索引:
for index := range numbers {
    fmt.Printf("索引 = %d\n", index)
}

当然 continue 也是可以用的。

for i := 0; i < 10; i++ {
    if i%2 == 0 {
        continue // 跳过偶数
    }
    fmt.Printf("奇数: %d\n", i)
}

嵌套循环

for i := 1; i <= 9; i++ {
    for j := 1; j <= i; j++ {
        fmt.Printf("%d*%d=%d\t", j, i, i*j)
    }
    fmt.Println()
}

总之,Golang 在 for 循环上的语法和 C 差不太多。


复合/引用数据结构(Composite Types)

这些不是 “基本类型”,但属于 Go 内置的核心数据结构。

  1. 数组 (Array):长度固定(长度是类型的一部分)。属于值类型(赋值会拷贝整个数组)。数组很少直接使用,通常用 slice 替代

    var arr [3]int = [3]int{1, 2, 3}
    // 或
    arr2 := [3]string{"a", "b", "c"}
    
  2. 切片 (Slice):最常用的数据类型。动态长度,底层基于数组。属于引用类型(传递的是 header,包含指针、长度、容量)。append 是内置函数,用于向 slice 添加元素

    s := []int{1, 2, 3}
    s = append(s, 4) // 动态扩容
    
  3. 映射 (Map):当字典用。是一种键值对集合,类似 Python | dictJava | HashMap。属于引用类型,必须用 make 初始化(或字面量),并且映射 (Map) 的 key 必须是可比较类型(不能是 slice/map/function)

    m := make(map[string]int)
    m["age"] = 25
    
  4. 结构体 (Struct):自定义复合类型,组合多个字段

    type Person struct {
        Name string
        Age  int
    }
    
    p := Person{Name: "Alice", Age: 30}
    
  5. 指针 (Pointer):存储变量的内存地址。通过 & 取地址,* 解引用。由于 Golang 没有指针运算(不能 p++),更安全。

    x := 42
    p := &x      // p 是 *int
    *p = 100     // 修改 x 的值
    
  6. 函数类型 (Function):在 Golang 中,函数是一种数据类型,可作为变量、参数、返回值

  7. 通道 (Channel):并发相关。用于 goroutine 之间通信

    ch := make(chan int)
    go func() { ch <- 42 }()
    value := <-ch
    
  8. 错误类型 (error)error 是内置接口,用于错误处理

  9. 其他类型:空接口类型 interface{} 可表示任意类型,在 Go 1.18+ 之后推荐用 any,实质上二者是等价的

指针、切片、map、channel、函数、接口这些引用类型的零值为 nilnil 源于Lisp 语言,nil 不是关键字,而是一个无类型的零值常量。


函数(Function)

函数定义的基本语法格式:

func funcName(varName1 varType1, varName2 varType2) returnType {
    Statement1;
    Statement2;
    ...
    return Expression
}

例如:

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

相同类型的多个参数,参数类型声明可以简化。例如上面例子中的 (a int, b int) 可以简化为 (a, b int)

在 Go 中还有一点需要注意的是,所有参数传递和赋值都是按值复制的 (pass by value)。


变参函数(Variadic Function)

参数部分 numbers ...int 表示这是一个可变参数函数,意思是你可以传入任意数量的 int 类型参数(包括0个)

func sum(numbers ...int) int {
    total := 0
    for _, num := range numbers {
        total += num
    }
    return total
}

最关键的一句是 for _, num := range numbers。首先使用 for range 循环遍历 numbers(它在函数内部其实是一个 []int 切片)。_ 是 Go 中的空白标识符,表示我们忽略索引(因为这里不需要知道第几个元素)。num 是当前遍历到的数值。如果你学习过 Python,这句话表达的意思是 for num in numbers(for Python)。


返回值(Return)

在之前的函数定义部分,其中第三个 int 表示返回值为 int 类型,且此时函数只有一个返回值。

                       vvv
func add(a int, b int) int {
    return a + b
}

在函数有多个返回值的情况下,返回值的类型声明要写成如下的格式:

                    vvvvvvvvvv
func swap(a, b int) (int, int) {
    return b, a
}

另外,有时候在返回值声明内会指定返回值的名称,这个部分表示命名返回值(可以在函数内部直接使用):

func divide(a, b int) (quotient, remainder int) {
    quotient = a / b
    remainder = a % b
    return  // 裸返回(bare return),自动返回所有命名返回值
}
fmt.println(divide(3, 1))  // 3 0

// 等价于
func divide2(a, b int) (int, int) {
    var quotient int = a / b
    var remainder int = a % b
    return quotient, remainder
}

但很显然,上面的写法更加浓缩,在首行暴露的信息也更精炼。更推荐 func divide(a, b int) (quotient, remainder int) 这种写法。


函数值也是一种数据类型

在先前的小节提到过,函数值也是一种数据类型。因此其他数据类型能做的事情,函数值作为一种数据类型也能做。

函数递归

func factorial(n int) int {
    if n <= 1 {
        return 1
    }
    return n * factorial(n-1)
}

函数作为参数

func apply(op func(int, int) int, a, b int) int {
    return op(a, b)
}

函数作为返回值

func makeAdder(x int) func(int) int {
    // 返回一个闭包函数
    return func(y int) int {
        return x + y
    }
}

函数是一等公民(first-class citizen)函数值(function value)是一种数据类型,可以像其他值一样被赋值、传递、返回。(First-class citizen,编程术语,用来描述某种语言实体(如函数、类型、变量等)是否被语言本身充分支持,并能像其他基本值一样自由使用)


面向对象(OOP)

需要注意的是,不像 Java/Python 的一切皆对象,Golang 在设计上并没有 “对象” 这个概念,Go 是基于类型和值的静态语言。但这并不代表基于 Golang 的语法不能实现 OOP。

Go 的哲学是组合优于继承 (Composition over Inheritance)。通过 结构体 (struct)、方法 (method)、接口 (interface),以及组合 (composition) 可以实现 OOP 的核心思想:封装、多态、组合复用。并且 Golang 在设计上刻意避免了继承。

OOP 三大特性在 Go 中的实现:

  1. 封装(Encapsulation):通过 struct 字段首字母大小写控制可见性(大写导出,小写私有)
  2. 多态(Polymorphism):通过 interface 实现
  3. 复用(代码重用):通过 嵌入(embedding) 实现组合(不是继承!)

封装(Struct + 方法)

定义类型(类似“类”)

// 文件:person.go
package main

import "fmt"

// Person 类似一个“类”
type Person struct {
    name string // 小写:包内私有
    Age  int    // 大写:公开
}

// 方法(绑定到 Person 类型)
func (p Person) SayHello() {
    fmt.Printf("Hello, I'm %s, %d years old\n", p.name, p.Age)
}

// 提供“构造函数”(Go 没有内置构造函数,但可自定义)
func NewPerson(name string, age int) *Person {
    return &Person{name: name, Age: age}
}

实际调用过程:

//1 直接初始化 (不调用 NewPerson)
alice := Person{name: "Alice", Age: 25}  // 实例化一个 Person
alice.SayHello()  // 调用 SayHello 方法

//2 通过“构造函数”初始化(显式调用)
p2 := NewPerson("Bob", 25)  // 函数内部创建并返回一个 *Person 指针;

其中 p 是方法的接收者,是一个实例变量名。这部分也是方法 (Method) 与函数 (Function) 在语法形式上不同的地方,并且一个方法只能有一个接收者。

     vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
func (receiverVarName ReceiverType) MethodName() {
    functionBody
}

这里用的是 值接收者(p Person)。如果改成指针接收者:(p *Person),那么 p 就是指向原对象的指针,可以修改原始数据。在 Go 中,所有参数传递和赋值都是按值复制的 (pass by value)。两者的区别是:如果你传一个结构体,Go 会复制整个结构体;而如果你传一个指针,Go 会复制这个指针的值(地址),但指向的是同一个对象。这里的详细区别这里就不展开讲了。

TIPSname 是私有的,外部不能直接访问,只能通过方法操作(实现封装)。


多态(Interface)

定义接口

// 定义一个接口
type Greeter interface {
    SayHello()
}

多个类型实现同一接口

type Dog struct{ Name string }

func (d Dog) SayHello() {
    fmt.Printf("Woof! I'm %s\n", d.Name)
}

type Cat struct{ Name string }

func (c Cat) SayHello() {
    fmt.Printf("Meow! I'm %s\n", c.Name)
}

多态调用

func Greet(g Greeter) {
    g.SayHello() // 运行时决定调用哪个实现
}

func main() {
    p := NewPerson("Alice", 30)
    d := Dog{"Buddy"}
    c := Cat{"Whiskers"}

    Greet(p) // Hello, I'm Alice...
    Greet(d) // Woof! I'm Buddy
    Greet(c) // Meow! I'm Whiskers
}

这就是多态,同一个接口,不同行为。


代码复用(组合,Embedding)

Go 没有继承,但可以用 嵌入 (embedding) 实现 “字段和方法的提升”

// 示例:Employee “包含” Person
type Employee struct {
    Person       // 匿名嵌入(不是继承!)
    Company string
}

// Employee 自动获得 Person 的所有字段和方法
e := Employee{
    Person:  Person{name: "Bob", Age: 28},
    Company: "Google",
}

e.SayHello()        // 调用 Person 的方法
fmt.Println(e.Age)  // 直接访问提升的字段

值得注意的是,Employee 不是 Person 的子类,EmployeePerson 是两个独立的类型。嵌入本质上只是语法糖,是编译器自动 “提升” 字段和方法。

Go 没有方法覆盖(Override),但你可以重新定义同名方法

func (e Employee) SayHello() {
    fmt.Printf("Hi, I'm %s from %s\n", e.name, e.Company)
}

现在 e.SayHello() 调用的是 Employee 的版本,不是 Person 的。但这不是覆盖,而是新定义。如果想调用原始方法,需显式指定:

e.Person.SayHello() // 调用嵌入类型的原始方法

Go 的 interface 是隐式实现的 (Duck Typing,“如果它走起来像鸭子,叫起来像鸭子,那它就是鸭子。”)。你不需要显式声明 type Dog implements Greeter,只要 DogSayHello() 方法,它就自动满足 Greeter 接口。这种解耦的方式灵活且易于 mock 测试。

总之,Go 不支持传统 OOP,但通过 struct + method + interface + embedding 进行更简洁、更灵活的实现。 面向组合编程,它没有 class 的面向对象,而是实现了一种面向接口和组合的编程


异常(Error)

Go 语言没有 try-catch-finally 异常机制,而是采用 显式错误处理 的哲学。错误(error)在 Go 中是一个内置接口类型,任何实现了其 Error() string 方法的类型都可以作为错误值。

// error 接口的定义
type error interface {
    Error() string
}

这种设计强制开发者在编码时就考虑并处理错误路径,使程序的健壮性和可读性更强。


错误处理基本模式

函数通常将 error 作为最后一个返回值。调用者必须显式检查该值是否为 nilnil 表示无错误)。

// 标准库 os.Open 的签名
func Open(name string) (*File, error)

// 调用与处理
file, err := os.Open("config.txt")
if err != nil {
    // 处理错误:记录日志、返回、退出等
    log.Fatal(err)
}
// 只有 err == nil 时,file 才是有效的
defer file.Close()

永远不要忽略错误!即使你认为“这里不可能出错”,也至少应该记录它。


创建错误

Go 提供了多种创建错误的方式:

errors.New: 创建一个简单的、仅包含静态字符串的错误。

import "errors"

func checkAge(age int) error {
    if age < 0 {
        return errors.New("年龄不能为负数")
    }
    return nil
}

fmt.Errorf: 创建格式化的错误信息,非常常用。

import "fmt"

func divide(a, b float64) (float64, error) {
    if b == 0.0 {
        return 0, fmt.Errorf("除零错误: 无法计算 %f / %f", a, b)
    }
    return a / b, nil
}

自定义错误类型: 当需要携带更多上下文信息(如错误码、字段名等)时,可以定义自己的结构体并实现 error 接口。

type ValidationError struct {
    Field   string
    Message string
}

// 实现 error 接口
func (e ValidationError) Error() string {
    return fmt.Sprintf("字段 '%s': %s", e.Field, e.Message)
}

func validateEmail(email string) error {
    if !isValid(email) {
        return ValidationError{
            Field:   "email",
            Message: "邮箱格式不正确",
        }
    }
    return nil
}

错误包装与检查(Go 1.13+)

为了在多层函数调用中保留原始错误信息,Go 支持错误包装 (Error Wrapping)

包装错误: 使用 fmt.Errorf 并配合 %w 动词。

func processFile(path string) error {
    data, err := ioutil.ReadFile(path)
    if err != nil {
        // 包装底层错误,添加上下文
        return fmt.Errorf("读取文件 %s 失败: %w", path, err)
    }
    // ... 处理 data
    return nil
}

检查错误链:

errors.Is(err, target): 检查错误链中是否包含特定的目标错误(即使被包装过)。

// 定义一个包级别的错误变量用于比较
var ErrNotFound = errors.New("未找到")

if errors.Is(err, ErrNotFound) {
    // 处理“未找到”的情况
}

errors.As(err, &target): 将错误链中的某个具体类型错误提取出来。

var validationErr ValidationError
if errors.As(err, &validationErr) {
    // 现在可以访问 validationErr.Field 和 .Message
    fmt.Printf("验证失败字段: %s\n", validationErr.Field)
}

关键要点总结

  1. 显式处理: 错误是值,必须被检查。
  2. nil 即成功: error 返回值为 nil 表示操作成功。
  3. 包装优于替换: 在向上层返回错误时,优先使用 fmt.Errorf + %w 进行包装,以保留完整的错误上下文。
  4. 使用 errors.Iserrors.As: 这是现代 Go 中检查和转换错误的标准方式,比类型断言更安全、更灵活。
  5. 提供有意义的信息: 错误信息应清晰、简洁,并包含足够的上下文(如文件名、参数值等)以便于调试。

通过这套简单而强大的机制,Go 鼓励开发者编写出对错误路径有充分考量的、可靠的代码。