错误处理 (Error Handling)
Go 语言中,错误处理是一种显式的、经过深思熟虑的设计。它没有采用其他语言中普遍的 try-catch
异常机制,而是将错误作为普通的值来返回。
error
接口
error
是 Go 中的一个内置接口类型,其定义极其简单:
type error interface {
Error() string
}
任何实现了 Error() string
方法的类型,都可以被当作一个 error
。这使得创建自定义错误类型变得非常灵活。
基本的错误处理模式
函数通常返回一个结果和一个 error
值。调用者必须显式检查返回的 error
是否为 nil
。
package main
import (
"fmt"
"strconv"
)
func main() {
// Atoi 返回一个 int 和一个 error
num, err := strconv.Atoi("123")
if err != nil {
// 如果 err 不是 nil,说明发生了错误,必须处理
fmt.Printf("An error occurred: %v\n", err)
return
}
fmt.Printf("Successfully converted number: %d\n", num)
}
这种模式强制开发者正视每一个可能出错的地方,有助于编写出更健壮的代码。
自定义错误类型
通过实现 error
接口,我们可以创建包含更多上下文信息的自定义错误类型。
package main
import (
"fmt"
"time"
)
// MyError 是一个自定义错误类型
type MyError struct {
When time.Time
What string
}
// 实现 error 接口
func (e *MyError) Error() string {
return fmt.Sprintf("at %v, %s", e.When, e.What)
}
func run() error {
return &MyError{
When: time.Now(),
What: "something went wrong",
}
}
func main() {
if err := run(); err != nil {
fmt.Println(err)
}
}
错误包装与解包
错误包装与解包 (Wrapping and Unwrapping)
自 Go 1.13 起,fmt.Errorf
函数支持使用 %w
动词来包装一个错误,从而形成一个错误链。这使得我们可以在不丢失原始错误上下文的情况下,添加新的上下文信息。
errors
包提供了 Is
和 As
函数来检查错误链:
errors.Is(err, target)
: 判断err
(或其包装链中的任何错误)是否与target
错误实例“是”同一个。errors.As(err, target)
: 判断err
(或其包装链中的任何错误)是否能被“视为”target
类型,并如果是,则将err
的值赋给target
。
package main
import (
"errors"
"fmt"
"os"
)
var ErrCustom = errors.New("a custom error")
func readFile() error {
// 模拟文件未找到错误
err := os.ErrNotExist
// 将 os.ErrNotExist 包装起来,并添加新的上下文
return fmt.Errorf("failed to read config: %w", err)
}
func main() {
err := readFile()
if err != nil {
// 使用 errors.Is 检查错误链中是否包含 os.ErrNotExist
if errors.Is(err, os.ErrNotExist) {
fmt.Println("File does not exist. Specific error:", err)
} else {
fmt.Println("An unknown error occurred:", err)
}
}
}
反射 (Reflection)
反射是指程序在运行时检查自身结构和行为的能力,例如获取一个变量的类型、值、方法等。Go 的 reflect
包提供了这一能力。反射虽然强大,但也应谨慎使用,因为它会牺牲编译时类型安全,且性能通常低于静态代码。
Type和Value
reflect
包的核心是两个类型:reflect.Type
和 reflect.Value
reflect.Type
: 表示一个值的类型信息。reflect.Value
: 表示一个值的实际内容。
可以通过 reflect.TypeOf()
和 reflect.ValueOf()
函数从一个 interface{}
中获取它们。
package main
import (
"fmt"
"reflect"
)
func inspect(i interface{}) {
t := reflect.TypeOf(i)
v := reflect.ValueOf(i)
fmt.Printf("Type: %s, Kind: %s\n", t.Name(), t.Kind())
fmt.Printf("Value: %v\n", v)
// Kind 用于获取底层的类型类别,如 struct, int, string 等
if t.Kind() == reflect.Struct {
fmt.Println("Fields:")
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
value := v.Field(i).Interface()
fmt.Printf(" - %s (%s) = %v\n", field.Name, field.Type, value)
}
}
}
type Person struct {
Name string
Age int
}
func main() {
p := Person{Name: "Alice", Age: 30}
inspect(p)
}
通过反射修改值
要通过反射修改一个变量的值,必须满足两个条件:
- 传入
reflect.ValueOf()
的必须是指针。 - 必须使用
Elem()
方法来获取指针指向的元素,然后才能调用Set
系列方法。
package main
import (
"fmt"
"reflect"
)
func setValue(i interface{}, value int) {
v := reflect.ValueOf(i)
// 检查是否是指针,以及指针是否为 nil
if v.Kind() != reflect.Ptr || v.IsNil() {
fmt.Println("Error: requires a non-nil pointer")
return
}
// 获取指针指向的元素,并设置新值
v.Elem().SetInt(int64(value))
}
func main() {
x := 10
fmt.Printf("Before: x = %d\n", x)
// 必须传入 x 的地址
setValue(&x, 100)
fmt.Printf("After: x = %d\n", x)
}
应用场景: ORM 框架、编解码库(如 encoding/json
)、依赖注入容器等,这些需要在运行时动态处理未知类型的场景。
CGO 混合编程
CGO 是 Go 语言与 C 语言进行互操作的工具。它允许你在 Go 代码中调用 C 代码,反之亦然。这对于复用现有的 C 库或在性能敏感的路径上使用 C 语言实现至关重要。
启用 CGO
要使用 CGO,只需在 Go 文件中 import "C"
即可。这个特殊的导入语句会指示 Go 工具链启用 CGO 处理。
紧跟在 import "C"
之前的注释块,会被 CGO 解释为 C 代码。
Go 调用 C
示例:调用 C 的 printf
函数
package main
/*
#include <stdio.h>
void myCFunction(const char* s) {
printf("Message from C: %s\n", s);
}
*/
import "C"
import "unsafe"
func main() {
message := "Hello from Go!"
// 将 Go string 转换为 C 的 char*
cMessage := C.CString(message)
// C.CString 分配的内存需要手动释放
defer C.free(unsafe.Pointer(cMessage))
// 调用 C 函数
C.myCFunction(cMessage)
}
编译和运行: 直接使用 go run
或 go build
即可,Go 工具链会自动处理 C 代码的编译和链接。
注意事项:
- 类型转换: Go 和 C 的类型系统不同,必须进行显式转换。例如,
string
->C.CString()
,int
->C.int
。 - 内存管理: 在 C 中分配的内存(如
C.CString
返回的指针)不受 Go GC 的管理,必须手动调用C.free
释放,否则会造成内存泄漏。unsafe.Pointer
常用于在 Go 和 C 指针之间转换。
C 调用 Go
CGO 也支持将 Go 函数导出,供 C 代码调用。这需要使用 //export
指令。
示例:将 Go 函数导出给 C 使用
mylib.go
:
package main
import "C"
import "fmt"
//export GoFunction
func GoFunction(s *C.char) {
goStr := C.GoString(s)
fmt.Printf("Go function received: %s\n", goStr)
}
func main() {} // CGO 库需要一个空的 main 函数
main.c
:
#include "mylib.h" // Go 编译时会生成一个 mylib.h 头文件
int main() {
// 调用导出的 Go 函数
GoFunction("Hello from C!");
return 0;
}
编译:
# 1. 将 Go 代码编译成 C 库
go build -o mylib.a -buildmode=c-archive mylib.go
# 2. 编译 C 代码,并链接 Go 库
gcc -o main main.c mylib.a -lpthread
CGO 是一个强大的工具,但它也带来了复杂性,包括性能开销(Go 与 C 之间的调用并非零成本)、更复杂的构建过程和内存管理责任。因此,只应在确实需要时使用。