Go 实战入门(一):从零搭建 Web API 项目
一、前言
作为一名后端开发者,学习一门新语言最痛苦的莫过于——看完语法教程,依然不知道怎么写项目。
本系列选择另一条路:直接从实战项目入手,在做项目的过程中学习 Go。不讲语法细节,只关注工程实践。
本篇是系列第一篇,目标是搭建一个完整的 Web API 项目骨架,包含:配置管理、日志系统、数据库操作、HTTP 路由、JWT 认证等核心模块。
完整代码见:go-practical-roadmap
二、项目结构
Go 项目没有强制的目录规范,但社区有一套约定俗成的布局:
01-web-api-template/
├── cmd/server/main.go # 程序入口
├── internal/ # 内部包(不对外暴露)
│ ├── api/ # HTTP 处理层
│ ├── service/ # 业务逻辑层
│ ├── repository/ # 数据访问层
│ ├── model/ # 数据模型
│ ├── config/ # 配置定义
│ ├── middleware/ # 中间件
│ └── app/ # 应用组装
├── pkg/ # 可复用的公共包
│ ├── logger/ # 日志封装
│ └── db/ # 数据库封装
├── configs/config.yaml # 配置文件
└── Makefile # 构建脚本
几个关键点:
- cmd/ 放可执行程序入口,每个子目录对应一个二进制文件
- internal/ 是 Go 的特殊目录,里面的包无法被外部项目导入
- pkg/ 放可以被其他项目复用的代码
三、程序入口与生命周期
main.go
package main
import (
"fmt"
"log"
"go-practical-roadmap/01-web-api-template/internal/app"
)
func main() {
// 创建应用实例
application, err := app.NewApp()
if err != nil {
log.Fatalf("Failed to create app: %v", err)
}
// 在 goroutine 中启动应用
go func() {
if err := application.Run(); err != nil {
log.Fatalf("Failed to run app: %v", err)
}
}()
fmt.Println("Server is running on :8080")
// 等待中断信号
application.WaitForInterrupt()
// 停止应用
if err := application.Stop(); err != nil {
log.Fatalf("Failed to stop app: %v", err)
}
}
为什么需要显式阻塞?
注意 application.WaitForInterrupt() 这行。Go 有个特性:main 函数结束 = 程序退出,不管还有多少 goroutine 在运行。
所以必须用某种方式”挡住” main 函数,否则 HTTP 服务器刚启动程序就退出了。
WaitForInterrupt 的实现很简单:
func (a *App) WaitForInterrupt() {
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit // 阻塞,直到收到 Ctrl+C
}
优雅停机
收到停止信号后,不能直接退出,要给正在处理的请求一个收尾的机会:
func (a *App) Stop() error {
// 创建 30 秒超时的 context
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// 优雅关闭 HTTP 服务(等待请求处理完)
if err := a.server.Shutdown(ctx); err != nil {
return err
}
// 关闭数据库连接
db.Close()
// 刷新日志缓冲
logger.GlobalLogger.Sync()
return nil
}
context.WithTimeout 是 Go 的超时控制机制。Shutdown 会等待所有请求处理完毕,但最多等 30 秒,超时就强制关闭。
四、配置管理(Viper)
Viper 是 Go 最流行的配置库,支持 YAML、JSON、环境变量等多种来源。
配置结构体
type Config struct {
Server ServerConfig `mapstructure:"server"`
Database DatabaseConfig `mapstructure:"database"`
JWT JWTConfig `mapstructure:"jwt"`
Logger LoggerConfig `mapstructure:"logger"`
}
type ServerConfig struct {
Port int `mapstructure:"port"`
Host string `mapstructure:"host"`
Mode string `mapstructure:"mode"`
}
type DatabaseConfig struct {
Driver string `mapstructure:"driver"`
DSN string `mapstructure:"dsn"`
MaxIdleConns int `mapstructure:"max_idle_conns"`
MaxOpenConns int `mapstructure:"max_open_conns"`
}
mapstructure 标签告诉 Viper 如何将 YAML 字段映射到结构体。
加载配置
func LoadConfig() (*Config, error) {
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath("./configs")
// 设置默认值
viper.SetDefault("server.port", 8080)
viper.SetDefault("database.max_idle_conns", 10)
// 环境变量自动覆盖(前缀 APP_)
viper.SetEnvPrefix("APP")
viper.AutomaticEnv()
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
// 读取配置文件
if err := viper.ReadInConfig(); err != nil {
return nil, err
}
// 反序列化到结构体
var config Config
if err := viper.Unmarshal(&config); err != nil {
return nil, err
}
return &config, nil
}
对应的 config.yaml:
server:
port: 8080
host: localhost
mode: debug
database:
driver: sqlite
dsn: ./data/app.db
max_idle_conns: 10
max_open_conns: 100
jwt:
secret: your-secret-key
access_token_exp: 3600
环境变量 APP_DATABASE_DSN 会自动覆盖 database.dsn 的值,方便在不同环境部署。
五、日志系统(Zap)
Zap 是 Uber 开源的高性能日志库,特点是零内存分配和结构化日志。
初始化日志
func NewLogger(level string, format string, outputPath string) (Logger, error) {
// 解析日志级别
var zapLevel zapcore.Level
switch level {
case "debug":
zapLevel = zap.DebugLevel
case "info":
zapLevel = zap.InfoLevel
case "error":
zapLevel = zap.ErrorLevel
default:
zapLevel = zap.InfoLevel
}
// 配置编码器
encoderConfig := zap.NewProductionEncoderConfig()
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
var encoder zapcore.Encoder
if format == "json" {
encoder = zapcore.NewJSONEncoder(encoderConfig)
} else {
encoder = zapcore.NewConsoleEncoder(encoderConfig)
}
// 配置输出(支持日志轮转)
var output zapcore.WriteSyncer
if outputPath == "stdout" {
output = zapcore.AddSync(os.Stdout)
} else {
output = zapcore.AddSync(&lumberjack.Logger{
Filename: outputPath,
MaxSize: 100, // MB
MaxBackups: 10,
MaxAge: 30, // days
})
}
core := zapcore.NewCore(encoder, output, zapLevel)
logger := zap.New(core, zap.AddCaller())
return &zapLogger{logger: logger}, nil
}
使用方式
// 结构化日志
logger.Info("User login",
zap.String("username", "sean"),
zap.Int("user_id", 123),
zap.Duration("latency", time.Millisecond*50))
输出(JSON 格式):
{"level":"INFO","ts":"2024-01-01T12:00:00Z","caller":"api/routes.go:42","msg":"User login","username":"sean","user_id":123,"latency":"50ms"}
结构化日志的好处是方便后续用 ELK、Loki 等工具分析检索。
六、数据库(Gorm)
Gorm 是 Go 最流行的 ORM,支持 MySQL、PostgreSQL、SQLite 等数据库。
连接数据库
var globalDB *gorm.DB
func Connect(dsn string, driver string) error {
var err error
switch driver {
case "mysql":
globalDB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
case "postgres":
globalDB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
case "sqlite":
globalDB, err = gorm.Open(sqlite.Open(dsn), &gorm.Config{})
}
if err != nil {
return err
}
// 配置连接池
sqlDB, _ := globalDB.DB()
sqlDB.SetMaxIdleConns(10)
sqlDB.SetMaxOpenConns(100)
sqlDB.SetConnMaxLifetime(time.Hour)
return nil
}
Model 定义
type User struct {
ID uint `gorm:"primaryKey" json:"id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at,omitempty"`
Username string `gorm:"uniqueIndex;size:50;not null" json:"username"`
Email string `gorm:"uniqueIndex;size:100;not null" json:"email"`
Password string `gorm:"size:255;not null" json:"-"`
IsActive bool `gorm:"default:true" json:"is_active"`
}
几个要点:
gorm标签定义数据库约束(主键、索引、长度等)json标签定义 JSON 序列化行为,json:"-"表示忽略该字段gorm.DeletedAt实现软删除,删除记录时只更新该字段
Repository 层
type UserRepository interface {
Create(user *model.User) error
GetByID(id uint) (*model.User, error)
GetByUsername(username string) (*model.User, error)
Update(user *model.User) error
Delete(id uint) error
}
type userRepository struct {
db *gorm.DB
}
func NewUserRepository(db *gorm.DB) UserRepository {
return &userRepository{db: db}
}
func (r *userRepository) Create(user *model.User) error {
return r.db.Create(user).Error
}
func (r *userRepository) GetByUsername(username string) (*model.User, error) {
var user model.User
err := r.db.Where("username = ?", username).First(&user).Error
if err != nil {
return nil, err
}
return &user, nil
}
func (r *userRepository) Delete(id uint) error {
return r.db.Delete(&model.User{}, id).Error
}
接口 + 实现分离的设计,方便单元测试时 mock。
七、HTTP 服务(Gin)
Gin 是 Go 最流行的 Web 框架,性能优异且 API 简洁。
路由注册
func SetupRoutes(userService service.UserService) *gin.Engine {
r := gin.New()
// 内置中间件
r.Use(gin.Logger())
r.Use(gin.Recovery())
// 自定义中间件
r.Use(middleware.RequestTracerMiddleware())
r.Use(middleware.CORSMiddleware())
// 公开路由
r.GET("/health", healthCheck)
r.POST("/api/v1/register", func(c *gin.Context) {
registerHandler(c, userService)
})
r.POST("/api/v1/login", func(c *gin.Context) {
loginHandler(c, userService)
})
// 需要认证的路由组
authorized := r.Group("/")
authorized.Use(middleware.JWTAuthMiddleware)
{
authorized.GET("/api/v1/profile", profileHandler)
}
return r
}
请求处理
func registerHandler(c *gin.Context, userService service.UserService) {
var req dto.RegisterRequest
// 绑定 JSON 请求体到结构体
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 调用业务层
user, err := userService.Register(&req)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 返回响应
c.JSON(http.StatusCreated, gin.H{
"message": "User registered successfully",
"data": user,
})
}
c.ShouldBindJSON 会自动解析请求体并校验参数(配合 validator 标签),省去大量样板代码。
八、中间件
JWT 认证
type Claims struct {
UserID uint `json:"user_id"`
Username string `json:"username"`
jwt.RegisteredClaims
}
// 生成 Token
func GenerateToken(userID uint, username string) (string, error) {
expirationTime := time.Now().Add(time.Duration(config.GlobalConfig.JWT.AccessTokenExp) * time.Second)
claims := &Claims{
UserID: userID,
Username: username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationTime),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(config.GlobalConfig.JWT.Secret))
}
// 认证中间件
func JWTAuthMiddleware(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Missing Authorization header"})
c.Abort()
return
}
if !strings.HasPrefix(authHeader, "Bearer ") {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization format"})
c.Abort()
return
}
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
claims, err := ValidateToken(tokenString)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
c.Abort()
return
}
// 将用户信息存入 context,后续 handler 可以取用
c.Set("user_id", claims.UserID)
c.Set("username", claims.Username)
c.Next()
}
c.Abort() 会终止后续中间件和 handler 的执行,直接返回响应。
九、业务分层
Service 层
type UserService interface {
Register(req *dto.RegisterRequest) (*dto.UserProfileResponse, error)
Login(req *dto.LoginRequest) (string, error)
}
type userService struct {
userRepo repository.UserRepository
}
func NewUserService(userRepo repository.UserRepository) UserService {
return &userService{userRepo: userRepo}
}
func (s *userService) Register(req *dto.RegisterRequest) (*dto.UserProfileResponse, error) {
// 检查用户名是否已存在
if _, err := s.userRepo.GetByUsername(req.Username); err == nil {
return nil, errors.New("username already exists")
}
// 密码加密
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
// 创建用户
user := &model.User{
Username: req.Username,
Email: req.Email,
Password: string(hashedPassword),
}
if err := s.userRepo.Create(user); err != nil {
return nil, err
}
// 返回 DTO(不含密码)
return &dto.UserProfileResponse{
ID: user.ID,
Username: user.Username,
Email: user.Email,
}, nil
}
func (s *userService) Login(req *dto.LoginRequest) (string, error) {
user, err := s.userRepo.GetByUsername(req.Username)
if err != nil {
return "", errors.New("invalid username or password")
}
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
return "", errors.New("invalid username or password")
}
return middleware.GenerateToken(user.ID, user.Username)
}
分层职责
| 层 | 职责 |
|---|---|
| API/Handler | 处理 HTTP 请求响应,参数校验 |
| Service | 业务逻辑,事务控制 |
| Repository | 数据访问,SQL 操作 |
| Model | 数据库实体 |
| DTO | 接口传输对象,与 Model 分离 |
DTO 和 Model 分离的好处:
- 接口返回的字段可以灵活控制,不暴露敏感信息(如密码)
- 数据库结构变化不影响 API 契约
十、小结
本篇完成了一个 Web API 项目的基础骨架:
- 程序入口:理解 Go 的启动机制和优雅停机
- 配置管理:使用 Viper 加载 YAML 配置和环境变量
- 日志系统:使用 Zap 输出结构化日志
- 数据库:使用 Gorm 定义 Model 和 Repository
- HTTP 服务:使用 Gin 注册路由和处理请求
- 中间件:实现 JWT 认证
- 业务分层:Service 层处理业务逻辑