工程实践与常见陷阱
Go 的工程实践包括项目结构、依赖管理、测试、错误处理等,以及开发中常见的陷阱。
项目结构
标准项目布局
myapp/
├── cmd/ # 主应用程序
│ ├── server/ # 服务端入口
│ │ └── main.go
│ └── client/ # 客户端入口
│ └── main.go
├── internal/ # 私有应用和库代码
│ ├── auth/ # 认证模块
│ └── database/ # 数据库模块
├── pkg/ # 可被外部使用的库
│ └── utils/
├── api/ # API 定义文件
│ ├── http/ # HTTP API
│ └── grpc/ # gRPC API
├── web/ # Web 静态资源
├── configs/ # 配置文件
├── scripts/ # 构建、安装、分析脚本
├── test/ # 额外的测试数据和工具
├── docs/ # 设计文档
├── go.mod
└── go.sum模块划分原则
- cmd/:每个子目录一个可执行程序
- internal/:私有代码,外部无法导入
- pkg/:可复用的库代码
- api/:协议定义(OpenAPI、protobuf 等)
依赖管理
Go Modules(推荐):
# 初始化模块
go mod init github.com/user/myapp
# 添加依赖
go get github.com/gin-gonic/gin
# 添加指定版本
go get github.com/gin-gonic/gin@v1.9.0
# 更新依赖
go get -u ./...
go mod tidy
# 查看依赖
go list -m all
# 下载依赖
go mod download
# 验证依赖
go mod verifygo.mod 示例:
module github.com/user/myapp
go 1.21
require (
github.com/gin-gonic/gin v1.9.0
github.com/go-redis/redis/v8 v8.11.5
)
exclude (
github.com/bad/dependency v1.0.0
)
replace (
github.com/local/dependency => ../local/dependency
)错误处理
Error 接口
// error 接口
type error interface {
Error() string
}
// 创建错误
err := errors.New("something went wrong")
// 格式化错误
err := fmt.Errorf("invalid value: %d", value)错误包装(Go 1.13+)
// 包装错误(添加上下文)
if err != nil {
return fmt.Errorf("failed to open file: %w", err)
}
// 解包错误
if errors.Is(err, os.ErrNotExist) {
// 文件不存在
}
// 类型断言
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Println("Path:", pathErr.Path)
}自定义错误
// 自定义错误类型
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed: %s %s", e.Field, e.Message)
}
// 使用
func Validate(user User) error {
if user.Name == "" {
return &ValidationError{
Field: "name",
Message: "required",
}
}
return nil
}
// 处理
if err := Validate(user); err != nil {
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Printf("Field %s: %s\n", ve.Field, ve.Message)
}
}Panic vs Error
// ❌ 不要用 panic 处理可预见的错误
func Divide(a, b int) int {
if b == 0 {
panic("division by zero") // 错误
}
return a / b
}
// ✅ 使用 error
func Divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}何时使用 panic:
- 不可恢复的错误(配置错误)
- 程序初始化失败
- 断言失败(开发阶段)
何时使用 error:
- 可预见的错误(网络、IO、业务逻辑)
- 需要上层处理的错误
Defer + Recover
func SafeOperation() (err error) {
defer func() {
if r := recover(); r != nil {
// 记录 panic
log.Printf("panic recovered: %v", r)
// 转换为 error
err = fmt.Errorf("panic: %v", r)
}
}()
// 可能 panic 的代码
panic("something bad")
}
func main() {
if err := SafeOperation(); err != nil {
log.Printf("error: %v", err)
}
}测试
单元测试
// calculator_test.go
package main
import "testing"
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive", 1, 2, 3},
{"negative", -1, -2, -3},
{"mixed", 1, -2, -1},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Add(tt.a, tt.b)
if result != tt.expected {
t.Errorf("Add(%d, %d) = %d; want %d",
tt.a, tt.b, result, tt.expected)
}
})
}
}表驱动测试
func TestValidate(t *testing.T) {
tests := []struct {
name string
input User
wantErr bool
}{
{
name: "valid",
input: User{Name: "test", Age: 20},
wantErr: false,
},
{
name: "empty name",
input: User{Name: "", Age: 20},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := Validate(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}并发测试
func TestCounterConcurrency(t *testing.T) {
counter := NewCounter()
iterations := 1000
// 启动多个 goroutine
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < iterations; j++ {
counter.Increment()
}
}()
}
wg.Wait()
expected := 10 * iterations
if counter.Value() != expected {
t.Errorf("Counter = %d; want %d", counter.Value(), expected)
}
}Mock 测试
// 使用接口 mock
type Database interface {
GetUser(id int) (*User, error)
}
type MockDatabase struct {
GetUserFunc func(id int) (*User, error)
}
func (m *MockDatabase) GetUser(id int) (*User, error) {
return m.GetUserFunc(id)
}
func TestService(t *testing.T) {
mockDB := &MockDatabase{
GetUserFunc: func(id int) (*User, error) {
return &User{Name: "test"}, nil
},
}
service := NewService(mockDB)
user, err := service.GetUser(1)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if user.Name != "test" {
t.Errorf("got name %s; want test", user.Name)
}
}常见陷阱
1. nil 切片 vs 空切片
var s1 []int // nil 切片
s2 := []int{} // 空切片
s3 := make([]int, 0) // 空
// len(s1) == len(s2) == 0
// s1 == nil, s2 != nil, s3 != nil
// JSON 序列化
json.Marshal(s1) // null
json.Marshal(s2) // []2. range 迭代变量
// ❌ 错误:迭代变量被复用
funcs := []func(){}
for _, v := range []int{1, 2, 3} {
funcs = append(funcs, func() {
fmt.Println(v) // 所有函数都输出 3
})
}
// ✅ 正确:创建局部变量
funcs := []func(){}
for _, v := range []int{1, 2, 3} {
val := v // 局部变量
funcs = append(funcs, func() {
fmt.Println(val)
})
}3. goroutine 循环变量
// ❌ 错误:所有 goroutine 使用最后一个值
for i := 0; i < 3; i++ {
go func() {
fmt.Println(i) // 输出 3, 3, 3
}()
}
// ✅ 正确:传递参数
for i := 0; i < 3; i++ {
go func(n int) {
fmt.Println(n) // 输出 0, 1, 2
}(i)
}4. channel 关闭
// ❌ 错误:关闭已关闭的 channel
var ch chan int
close(ch)
close(ch) // panic
// ✅ 正确:使用 sync.Once
var once sync.Once
once.Do(func() {
close(ch)
})5. defer 执行顺序
// defer 是 LIFO(后进先出)
func example() {
defer fmt.Println(1)
defer fmt.Println(2)
defer fmt.Println(3)
// 输出:3, 2, 1
}6. slice 扩容
// 切片扩容会创建新数组
s1 := []int{1, 2, 3}
s2 := s1[:2]
s2 = append(s2, 99) // 不影响 s1
s1 = append(s1, 4) // 可能触发扩容,创建新数组7. Map 并发读写
// ❌ 错误:Map 并发不安全
var m = make(map[string]int)
go func() { m["key"] = 1 }()
go func() { _ = m["key"] }() // panic
// ✅ 正确:使用 sync.Map 或加锁
var mu sync.Mutex
var m = make(map[string]int)
go func() {
mu.Lock()
m["key"] = 1
mu.Unlock()
}()
go func() {
mu.Lock()
_ = m["key"]
mu.Unlock()
}()8. interface nil
// 返回 nil interface
func returnsError() error {
var p *MyError = nil
return p // 返回的是 nil 指针,不是 nil interface
}
// 检查
err := returnsError()
fmt.Println(err == nil) // false
fmt.Println(err) // <nil>最佳实践
1. 错误处理
// ✅ 尽早处理错误
func Process() error {
data, err := fetchData()
if err != nil {
return fmt.Errorf("fetch data: %w", err)
}
result, err := process(data)
if err != nil {
return fmt.Errorf("process: %w", err)
}
return save(result)
}2. 资源清理
// ✅ 使用 defer 确保资源释放
func Process(filename string) error {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close()
// 处理文件
return nil
}3. 并发安全
// ✅ 使用 channel 通信
type Counter struct {
ch chan func()
}
func NewCounter() *Counter {
c := &Counter{ch: make(chan func())}
go c.run()
return c
}
func (c *Counter) run() {
count := 0
for f := range c.ch {
f()
}
}
func (c *Counter) Increment() {
c.ch <- func() {
count++
}
}4. 配置管理
// ✅ 使用结构体配置
type Config struct {
Host string
Port int
Timeout time.Duration
}
func NewConfig() *Config {
return &Config{
Host: "localhost",
Port: 8080,
Timeout: 30 * time.Second,
}
}
// 支持环境变量覆盖
func (c *Config) LoadFromEnv() {
if host := os.Getenv("HOST"); host != "" {
c.Host = host
}
// ...
}常见面试题
1. Go 项目如何组织?
- cmd/:主程序入口
- internal/:私有代码
- pkg/:可复用库
- api/:API 定义
- 使用 go modules 管理依赖
2. 错误处理最佳实践?
- 尽早处理错误
- 添加上下文(fmt.Errorf + %w)
- 自定义错误类型
- panic 只用于不可恢复的错误
- 使用 errors.Is 和 errors.As 检查错误
3. 如何编写可测试的代码?
- 使用接口解耦依赖
- 依赖注入
- 避免全局状态
- 小函数、单一职责
- 表驱动测试
4. 常见的 Go 陷阱?
- nil 切片 vs 空切片
- range 迭代变量复用
- goroutine 循环变量捕获
- channel 关闭 panic
- map 并发读写
练习题
- 实现一个带超时的 HTTP 客户端
- 实现一个简单的日志库(支持日志级别)
- 编写一个线程安全的缓存(LRU)
- 实现一个简单的 worker pool
