第十章:项目部署与最佳实践
10.1 项目结构
标准项目布局
myproject/
├── cmd/ # 应用程序入口
│ ├── server/ # 主服务
│ │ └── main.go
│ └── worker/ # 后台任务
│ └── main.go
├── internal/ # 私有代码
│ ├── config/ # 配置
│ ├── handlers/ # HTTP处理器
│ ├── models/ # 数据模型
│ ├── repository/ # 数据访问层
│ └── service/ # 业务逻辑层
├── pkg/ # 公共库(可被外部使用)
│ ├── utils/
│ └── middleware/
├── api/ # API定义
│ └── proto/
├── web/ # 前端资源
│ ├── static/
│ └── templates/
├── configs/ # 配置文件
├── scripts/ # 脚本
├── deployments/ # 部署配置
├── docs/ # 文档
├── tests/ # 测试
├── go.mod
├── go.sum
├── Makefile
├── Dockerfile
└── README.md示例项目结构
// cmd/api/main.go
package main
import (
"log"
"myproject/internal/config"
"myproject/internal/server"
)
func main() {
cfg := config.Load()
srv := server.New(cfg)
if err := srv.Run(); err != nil {
log.Fatal(err)
}
}10.2 配置管理
使用Viper
package config
import (
"github.com/spf13/viper"
)
type Config struct {
Server ServerConfig
Database DatabaseConfig
Redis RedisConfig
}
type ServerConfig struct {
Port int
Mode string
}
type DatabaseConfig struct {
Host string
Port int
User string
Password string
DBName string
}
type RedisConfig struct {
Host string
Port int
}
func Load() *Config {
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath(".")
viper.AddConfigPath("./configs")
// 默认值
viper.SetDefault("server.port", 8080)
viper.SetDefault("server.mode", "release")
// 环境变量
viper.AutomaticEnv()
if err := viper.ReadInConfig(); err != nil {
panic(err)
}
var cfg Config
if err := viper.Unmarshal(&cfg); err != nil {
panic(err)
}
return &cfg
}配置文件示例
# configs/config.yaml
server:
port: 8080
mode: debug
database:
host: localhost
port: 3306
user: root
password: secret
dbname: mydb
redis:
host: localhost
port: 637910.3 日志管理
使用Zap
package logger
import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
var Log *zap.Logger
func Init() {
config := zap.NewProductionConfig()
config.EncoderConfig.TimeKey = "timestamp"
config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
var err error
Log, err = config.Build()
if err != nil {
panic(err)
}
}
func Info(msg string, fields ...zap.Field) {
Log.Info(msg, fields...)
}
func Error(msg string, fields ...zap.Field) {
Log.Error(msg, fields...)
}
func Fatal(msg string, fields ...zap.Field) {
Log.Fatal(msg, fields...)
}10.4 错误处理
自定义错误
package errors
import (
"fmt"
)
type ErrorCode int
const (
ErrCodeNotFound ErrorCode = iota + 1000
ErrCodeInvalidParam
ErrCodeUnauthorized
ErrCodeInternal
)
type AppError struct {
Code ErrorCode
Message string
Err error
}
func (e *AppError) Error() string {
if e.Err != nil {
return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Err)
}
return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}
func NewNotFound(msg string) *AppError {
return &AppError{
Code: ErrCodeNotFound,
Message: msg,
}
}
func NewInvalidParam(msg string) *AppError {
return &AppError{
Code: ErrCodeInvalidParam,
Message: msg,
}
}
func Wrap(err error, code ErrorCode, msg string) *AppError {
return &AppError{
Code: code,
Message: msg,
Err: err,
}
}10.5 测试
单元测试
// calculator.go
package calculator
func Add(a, b int) int {
return a + b
}
func Divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("除数不能为0")
}
return a / b, nil
}
// calculator_test.go
package calculator
import (
"testing"
)
func TestAdd(t *testing.T) {
tests := []struct {
a, b, want int
}{
{1, 2, 3},
{0, 0, 0},
{-1, 1, 0},
}
for _, tt := range tests {
got := Add(tt.a, tt.b)
if got != tt.want {
t.Errorf("Add(%d, %d) = %d, want %d", tt.a, tt.b, got, tt.want)
}
}
}
func TestDivide(t *testing.T) {
_, err := Divide(10, 0)
if err == nil {
t.Error("期望错误但没有返回")
}
result, err := Divide(10, 2)
if err != nil {
t.Errorf("不期望错误: %v", err)
}
if result != 5 {
t.Errorf("期望5,得到%d", result)
}
}基准测试
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(100, 200)
}
}运行测试
# 运行所有测试
go test ./...
# 运行特定测试
go test -run TestAdd
# 显示覆盖率
go test -cover
# 生成覆盖率报告
go test -coverprofile=coverage.out
go tool cover -html=coverage.out
# 运行基准测试
go test -bench=.
go test -bench=. -benchmem10.6 Docker部署
Dockerfile
# 构建阶段
FROM golang:1.21-alpine AS builder
WORKDIR /app
# 安装依赖
RUN apk add --no-cache git
# 复制依赖文件
COPY go.mod go.sum ./
RUN go mod download
# 复制源码
COPY . .
# 构建
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o server ./cmd/server
# 运行阶段
FROM alpine:latest
WORKDIR /app
# 安装ca证书
RUN apk --no-cache add ca-certificates
# 从构建阶段复制二进制文件
COPY --from=builder /app/server .
COPY --from=builder /app/configs ./configs
# 暴露端口
EXPOSE 8080
# 运行
CMD ["./server"]docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "8080:8080"
environment:
- SERVER_PORT=8080
- DATABASE_HOST=db
depends_on:
- db
- redis
restart: unless-stopped
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: secret
MYSQL_DATABASE: myapp
volumes:
- mysql_data:/var/lib/mysql
ports:
- "3306:3306"
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
mysql_data:10.7 Makefile
.PHONY: build test clean run docker-build docker-run
# 变量
APP_NAME=myapp
BUILD_DIR=./build
# 构建
build:
mkdir -p $(BUILD_DIR)
go build -o $(BUILD_DIR)/$(APP_NAME) ./cmd/server
# 测试
test:
go test -v ./...
# 测试覆盖率
test-coverage:
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html
# 清理
clean:
rm -rf $(BUILD_DIR)
rm -f coverage.out coverage.html
# 运行
run:
go run ./cmd/server
# 开发模式(热重载)
dev:
air
# 格式化代码
fmt:
go fmt ./...
# 代码检查
lint:
golangci-lint run
# 下载依赖
deps:
go mod download
go mod tidy
# Docker构建
docker-build:
docker build -t $(APP_NAME):latest .
# Docker运行
docker-run:
docker-compose up -d
# Docker停止
docker-stop:
docker-compose down10.8 CI/CD配置
GitHub Actions
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.21'
- name: Cache dependencies
uses: actions/cache@v3
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
- name: Download dependencies
run: go mod download
- name: Run tests
run: go test -v -race -coverprofile=coverage.out ./...
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage.out
- name: Build
run: go build -v ./...
- name: Run linter
uses: golangci/golangci-lint-action@v3
with:
version: latest10.9 性能优化
pprof性能分析
package main
import (
"net/http"
_ "net/http/pprof"
)
func main() {
// 启动pprof服务
go func() {
http.ListenAndServe("localhost:6060", nil)
}()
// 主程序...
}分析命令
# CPU分析
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
# 内存分析
go tool pprof http://localhost:6060/debug/pprof/heap
# Goroutine分析
go tool pprof http://localhost:6060/debug/pprof/goroutine
# 在pprof中
(pprof) top # 显示热点
(pprof) list func # 显示函数源码
(pprof) web # 生成可视化图10.10 最佳实践总结
代码规范
命名规范
包名:小写,简短
接口名:以
er结尾(如Reader,Writer)导出符号:大写开头
未导出符号:小写开头
错误处理
立即处理错误,不要忽略
错误信息要有上下文
使用自定义错误类型
并发安全
避免共享内存,使用channel通信
保护共享数据使用Mutex
注意goroutine泄漏
资源管理
使用defer关闭资源
使用context控制超时
避免内存泄漏
常用工具
# 代码格式化
go fmt ./...
# 代码检查
go vet ./...
# 静态分析
golangci-lint run
# 依赖分析
go mod graph
# 包文档
go doc package恭喜完成Go语言学习!
现在你已经掌握了Go语言的核心知识,可以开始构建自己的项目了。