Appearance
Docker 部署与微服务集成
本文从 Docker 化 Gin 应用出发,逐步讲解微服务架构下的服务通信、注册发现、API 网关与配置管理。所有方案均面向生产环境,包含可直接复用的 Dockerfile、docker-compose 与代码示例。
一、Docker 化 Gin 应用
1.1 编写 Dockerfile(多阶段构建)
Go 是编译型语言,构建产物是单一二进制文件,非常适合多阶段构建。最终镜像不需要 Go 工具链。
dockerfile
# 阶段1:构建
FROM golang:1.22-alpine AS builder
# 安装必要的工具(git 用于拉取依赖,ca-certificates 用于 HTTPS)
RUN apk add --no-cache git ca-certificates
WORKDIR /build
# 利用缓存:先复制 go.mod/go.sum,下载依赖
COPY go.mod go.sum ./
RUN GOPROXY=https://goproxy.cn,direct go mod download
# 复制源码
COPY . .
# 编译参数
ARG VERSION=dev
ARG BUILD_TIME
ARG CGO_ENABLED=0
# 静态编译,开启链接时优化(去除调试信息,减小体积)
RUN CGO_ENABLED=${CGO_ENABLED} GOOS=linux GOARCH=amd64 \
go build -ldflags="-s -w -X main.Version=${VERSION} -X main.BuildTime=${BUILD_TIME}" \
-o /app/server ./cmd/server
# 阶段2:运行时(使用 alpine)
FROM alpine:3.19
# 安装最小依赖
RUN apk --no-cache add ca-certificates tzdata && \
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
echo "Asia/Shanghai" > /etc/timezone && \
apk del tzdata
WORKDIR /app
# 复制二进制
COPY --from=builder /app/server .
# 复制静态资源(如有)
COPY --from=builder /build/configs ./configs
# 非 root 用户运行
RUN adduser -D -u 10001 appuser
USER appuser
EXPOSE 8080
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD wget -qO- http://localhost:8080/health || exit 1
ENTRYPOINT ["./server"]关键优化点:
- 多阶段构建:构建阶段使用
golang:1.22-alpine(约 400MB),最终镜像只有 alpine + 二进制(约 20-30MB)。 -ldflags="-s -w":去除调试信息与符号表,二进制体积减小 30%。CGO_ENABLED=0:纯静态编译,可在 scratch 镜像运行。- 缓存优化:先
COPY go.mod go.sum再go mod download,依赖不变时利用 Docker layer 缓存。 - 非 root 用户:安全最佳实践,避免容器逃逸后获得 root 权限。
- 时区配置:默认 alpine 是 UTC,国内服务需要设置时区。
1.2 优化镜像大小:scratch / alpine
dockerfile
# scratch 镜像:极致精简(约 15MB)
FROM scratch
# scratch 没有 CA 证书,需要从 builder 复制
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /app/server /server
# scratch 没有基础库,必须 CGO_ENABLED=0
ENTRYPOINT ["/server"]scratch 与 alpine 对比:
| 镜像 | 大小 | 调试能力 | 适用场景 |
|---|---|---|---|
| golang | ~900MB | 完整 Go 环境 | 开发调试 |
| alpine | ~30MB | 有 shell、apk | 生产通用 |
| scratch | ~15MB | 无 shell、无 libc | 极致优化、纯静态二进制 |
| distroless | ~25MB | 无 shell、有 libc | 安全敏感(Google 出品) |
scratch 的限制:
- 没有 shell,无法
docker exec -it xxx sh进入调试。 - 没有 libc,必须
CGO_ENABLED=0。 - 没有时区数据,需手动复制
zoneinfo。
1.3 docker-compose 编排
yaml
# docker-compose.yml
version: "3.9"
services:
app:
build:
context: .
dockerfile: Dockerfile
args:
VERSION: "1.0.0"
image: myapp/server:1.0.0
container_name: myapp-server
restart: unless-stopped
ports:
- "8080:8080"
environment:
- GIN_MODE=release
- DB_DSN=root:pass@tcp(mysql:3306)/myapp?parseTime=true
- REDIS_ADDR=redis:6379
- LOG_LEVEL=info
volumes:
- ./configs:/app/configs:ro
- app-logs:/app/logs
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_started
networks:
- myapp-net
deploy:
resources:
limits:
cpus: "2.0"
memory: 1G
reservations:
cpus: "0.5"
memory: 256M
mysql:
image: mysql:8.0
container_name: myapp-mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: pass
MYSQL_DATABASE: myapp
volumes:
- mysql-data:/var/lib/mysql
ports:
- "3306:3306"
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
networks:
- myapp-net
redis:
image: redis:7-alpine
container_name: myapp-redis
restart: unless-stopped
ports:
- "6379:6379"
volumes:
- redis-data:/data
networks:
- myapp-net
volumes:
mysql-data:
redis-data:
app-logs:
networks:
myapp-net:
driver: bridge二、微服务架构基础
2.1 单体到微服务的演进
单体架构的问题:
- 代码膨胀:所有功能在一个仓库,编译慢, IDE 卡顿。
- 发布耦合:一个小改动需要重新部署整个系统。
- 技术栈僵化:难以对单个模块使用不同语言/数据库。
- 扩展困难:高负载模块无法独立扩容。
微服务的核心思想:按业务能力拆分,独立部署,轻量通信。
单体架构:
┌─────────────────────────────┐
│ 用户服务 / 订单服务 / 商品服务 │
│ 共享 DB / 共享代码 / 共享部署 │
└─────────────────────────────┘
微服务架构:
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 用户服务 │ │ 订单服务 │ │ 商品服务 │
│ Gin │ │ Gin │ │ Gin │
│ MySQL │ │ MySQL │ │ MySQL │
└──────────┘ └──────────┘ └──────────┘
▲ ▲ ▲
│ HTTP/gRPC │ │
└─────────────┴─────────────┘
API 网关2.2 Gin 在微服务中的角色
Gin 适合作为微服务的 HTTP API 层:
- 对外 RESTful API(供前端、第三方调用)
- 内部 HTTP 通信(服务间简单调用)
- 健康检查、指标暴露、运维端点
对于高性能内部 RPC,推荐 gRPC(详见下节)。Gin 与 gRPC 可共存于同一服务。
三、gRPC 集成
3.1 gRPC 服务定义
定义 protobuf 文件:
protobuf
// proto/user.proto
syntax = "proto3";
package user;
option go_package = "./proto;userpb";
service UserService {
rpc GetUser(GetUserReq) returns (GetUserResp);
rpc ListUsers(ListUsersReq) returns (ListUsersResp);
rpc CreateUser(CreateUserReq) returns (CreateUserResp);
}
message GetUserReq {
int64 id = 1;
}
message User {
int64 id = 1;
string name = 2;
string email = 3;
int32 age = 4;
}
message GetUserResp {
User user = 1;
}
message ListUsersReq {
int32 page = 1;
int32 size = 2;
}
message ListUsersResp {
repeated User users = 1;
int32 total = 2;
}
message CreateUserReq {
string name = 1;
string email = 2;
int32 age = 3;
}
message CreateUserResp {
int64 id = 1;
}生成 Go 代码:
bash
protoc --go_out=. --go-grpc_out=. proto/user.proto实现 gRPC 服务:
go
package server
import (
"context"
"database/sql"
pb "myapp/proto"
)
type UserServer struct {
pb.UnimplementedUserServiceServer
db *sql.DB
}
func (s *UserServer) GetUser(ctx context.Context, req *pb.GetUserReq) (*pb.GetUserResp, error) {
var u pb.User
err := s.db.QueryRowContext(ctx,
"SELECT id, name, email, age FROM users WHERE id = ?", req.Id).
Scan(&u.Id, &u.Name, &u.Email, &u.Age)
if err == sql.ErrNoRows {
return nil, status.Errorf(codes.NotFound, "user not found")
}
if err != nil {
return nil, status.Errorf(codes.Internal, err.Error())
}
return &pb.GetUserResp{User: &u}, nil
}3.2 Gin 与 gRPC 共存:双端口服务
生产环境常见模式:对外 HTTP(Gin),对内 gRPC。
go
package main
import (
"context"
"log"
"net"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"google.golang.org/grpc"
"github.com/gin-gonic/gin"
)
func main() {
// 启动 gRPC 服务(内部 RPC)
go startGRPC(":9090")
// 启动 Gin HTTP 服务(对外 API)
go startHTTP(":8080")
// 优雅等待退出信号
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("shutting down...")
}
func startGRPC(addr string) {
lis, err := net.Listen("tcp", addr)
if err != nil {
log.Fatalf("gRPC listen: %v", err)
}
s := grpc.NewServer(
grpc.UnaryInterceptor(grpcUnaryInterceptor),
)
pb.RegisterUserServiceServer(s, &UserServer{db: db})
log.Printf("gRPC listening on %s", addr)
if err := s.Serve(lis); err != nil {
log.Fatalf("gRPC serve: %v", err)
}
}
func startHTTP(addr string) {
r := gin.New()
r.Use(gin.Recovery())
r.GET("/api/users/:id", func(c *gin.Context) {
id, _ := strconv.ParseInt(c.Param("id"), 10, 64)
// HTTP handler 可以直接调用本地 DB,或通过 gRPC 调用其他服务
user, err := getUser(c, id)
if err != nil {
c.JSON(500, gin.H{"err": err.Error()})
return
}
c.JSON(200, user)
})
server := &http.Server{
Addr: addr,
Handler: r,
}
log.Printf("HTTP listening on %s", addr)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("HTTP serve: %v", err)
}
}
// gRPC 拦截器:日志、链路追踪、错误处理
func grpcUnaryInterceptor(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (resp interface{}, err error) {
start := time.Now()
resp, err = handler(ctx, req)
log.Printf("gRPC %s %v err=%v", info.FullMethod, time.Since(start), err)
return
}3.3 gRPC-Gateway 集成
gRPC-Gateway 可以从 protobuf 自动生成 RESTful API,实现「一份 proto 定义,同时支持 gRPC 与 HTTP」。
bash
# 安装
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest
# 生成(在 proto 中添加 google.api.http 注解)
protoc \
--grpc-gateway_out=. \
--grpc-gateway_opt paths=source_relative \
proto/user.protoprotobuf 添加 HTTP 注解:
protobuf
import "google/api/annotations.proto";
service UserService {
rpc GetUser(GetUserReq) returns (GetUserResp) {
option (google.api.http) = {
get: "/api/users/{id}"
};
}
rpc CreateUser(CreateUserReq) returns (CreateUserResp) {
option (google.api.http) = {
post: "/api/users"
body: "*"
};
}
}启动 gRPC-Gateway:
go
func startGateway(grpcAddr, httpAddr string) {
ctx := context.Background()
mux := runtime.NewServeMux()
opts := []grpc.DialOption{grpc.WithInsecure()}
err := pb.RegisterUserServiceHandlerFromEndpoint(ctx, mux, grpcAddr, opts)
if err != nil {
log.Fatal(err)
}
log.Printf("Gateway listening on %s", httpAddr)
log.Fatal(http.ListenAndServe(httpAddr, mux))
}四、服务注册与发现
4.1 集成 Consul
Consul 提供服务注册、发现、健康检查一体化方案。
go
package registry
import (
"fmt"
"log"
"net"
"github.com/hashicorp/consul/api"
)
type ConsulRegistry struct {
client *api.Client
}
func NewConsul(addr string) (*ConsulRegistry, error) {
cfg := api.DefaultConfig()
cfg.Address = addr
client, err := api.NewClient(cfg)
if err != nil {
return nil, err
}
return &ConsulRegistry{client: client}, nil
}
type ServiceInfo struct {
ID string
Name string
Address string
Port int
Tags []string
Check *api.AgentServiceCheck
}
func (r *ConsulRegistry) Register(svc ServiceInfo) error {
reg := &api.AgentServiceRegistration{
ID: svc.ID,
Name: svc.Name,
Address: svc.Address,
Port: svc.Port,
Tags: svc.Tags,
Check: svc.Check,
}
return r.client.Agent().ServiceRegister(reg)
}
func (r *ConsulRegistry) Deregister(id string) error {
return r.client.Agent().ServiceDeregister(id)
}
func (r *ConsulRegistry) Discover(name string) ([]*api.AgentService, error) {
services, _, err := r.client.Health().Service(name, "", true, nil)
if err != nil {
return nil, err
}
result := make([]*api.AgentService, 0, len(services))
for _, s := range services {
result = append(result, s.Service)
}
return result, nil
}
// 在 main 中使用
func registerService() {
reg, _ := NewConsul("consul:8500")
// 获取本机 IP
ip := getOutboundIP()
port := 8080
svc := ServiceInfo{
ID: fmt.Sprintf("user-service-%s-%d", ip, port),
Name: "user-service",
Address: ip,
Port: port,
Tags: []string{"v1", "gin"},
Check: &api.AgentServiceCheck{
HTTP: fmt.Sprintf("http://%s:%d/health", ip, port),
Interval: "10s",
Timeout: "3s",
},
}
if err := reg.Register(svc); err != nil {
log.Fatal(err)
}
// 优雅注销
go func() {
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
reg.Deregister(svc.ID)
os.Exit(0)
}()
}4.2 集成 Etcd
Etcd 是 Kubernetes 的核心组件,也常用于服务发现。
go
package registry
import (
"context"
"fmt"
"time"
clientv3 "go.etcd.io/etcd/v3/clientv3"
)
type EtcdRegistry struct {
client *clientv3.Client
lease clientv3.LeaseID
}
func NewEtcd(endpoints []string) (*EtcdRegistry, error) {
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: 5 * time.Second,
})
if err != nil {
return nil, err
}
return &EtcdRegistry{client: cli}, nil
}
func (r *EtcdRegistry) Register(name, addr string, ttl int64) error {
// 申请租约
resp, err := r.client.Grant(context.Background(), ttl)
if err != nil {
return err
}
r.lease = resp.ID
key := fmt.Sprintf("/services/%s/%s", name, addr)
_, err = r.client.Put(context.Background(), key, addr, clientv3.WithLease(r.lease))
if err != nil {
return err
}
// 自动续约
ch, err := r.client.KeepAlive(context.Background(), r.lease)
if err != nil {
return err
}
go func() {
for {
select {
case _, ok := <-ch:
if !ok {
return
}
}
}
}()
return nil
}
func (r *EtcdRegistry) Deregister(name, addr string) error {
key := fmt.Sprintf("/services/%s/%s", name, addr)
_, err := r.client.Delete(context.Background(), key)
return r.client.Revoke(context.Background(), r.lease)
}
func (r *EtcdRegistry) Discover(name string) ([]string, error) {
prefix := fmt.Sprintf("/services/%s/", name)
resp, err := r.client.Get(context.Background(), prefix, clientv3.WithPrefix())
if err != nil {
return nil, err
}
addrs := make([]string, 0, len(resp.Kvs))
for _, kv := range resp.Kvs {
addrs = append(addrs, string(kv.Value))
}
return addrs, nil
}4.3 健康检查端点
go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
type HealthChecker interface {
Check() error
}
type HealthHandler struct {
checks map[string]HealthChecker
}
func NewHealthHandler() *HealthHandler {
return &HealthHandler{checks: make(map[string]HealthChecker)}
}
func (h *HealthHandler) Add(name string, c HealthChecker) {
h.checks[name] = c
}
// Liveness 存活检查:服务进程是否还在响应
func (h *HealthHandler) Liveness(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "alive"})
}
// Readiness 就绪检查:服务是否可以处理请求(依赖是否就绪)
func (h *HealthHandler) Readiness(c *gin.Context) {
status := http.StatusOK
result := gin.H{"status": "ok"}
failed := false
for name, checker := range h.checks {
if err := checker.Check(); err != nil {
result[name] = err.Error()
failed = true
} else {
result[name] = "ok"
}
}
if failed {
result["status"] = "not ready"
status = http.StatusServiceUnavailable
}
c.JSON(status, result)
}
// DB 健康检查
type DBChecker struct{ db *sql.DB }
func (c *DBChecker) Check() error {
return c.db.Ping()
}
// Redis 健康检查
type RedisChecker struct{ client *redis.Client }
func (c *RedisChecker) Check() error {
return c.client.Ping(context.Background()).Err()
}
// 使用
func main() {
r := gin.New()
hh := NewHealthHandler()
hh.Add("db", &DBChecker{db: db})
hh.Add("redis", &RedisChecker{client: rdb})
r.GET("/health", hh.Liveness)
r.GET("/ready", hh.Readiness)
r.Run()
}五、API 网关模式
5.1 反向代理中间件
Gin 可以作为轻量级 API 网关,通过 httputil.ReverseProxy 转发请求。
go
package gateway
import (
"net/http"
"net/http/httputil"
"net/url"
"github.com/gin-gonic/gin"
)
type Gateway struct {
routes map[string]string // path prefix -> backend URL
}
func New() *Gateway {
return &Gateway{
routes: map[string]string{
"/api/users": "http://user-service:8080",
"/api/orders": "http://order-service:8080",
"/api/goods": "http://goods-service:8080",
},
}
}
func (g *Gateway) Proxy() gin.HandlerFunc {
return func(c *gin.Context) {
backendURL := g.matchRoute(c.Request.URL.Path)
if backendURL == "" {
c.JSON(404, gin.H{"err": "no backend"})
return
}
target, _ := url.Parse(backendURL)
proxy := httputil.NewSingleHostReverseProxy(target)
// 自定义 Director:保留原始路径,注入 header
originalDirector := proxy.Director
proxy.Director = func(req *http.Request) {
originalDirector(req)
req.Header.Set("X-Forwarded-For", c.ClientIP())
req.Header.Set("X-Request-ID", c.GetString("request_id"))
// 注入用户身份(由鉴权中间件解析后设置)
if uid, ok := c.Get("user_id"); ok {
req.Header.Set("X-User-ID", fmt.Sprintf("%v", uid))
}
}
// 自定义错误处理:后端不可用时返回降级响应
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
log.Printf("proxy error: %v", err)
w.WriteHeader(http.StatusBadGateway)
w.Write([]byte(`{"err":"service unavailable"}`))
}
proxy.ServeHTTP(c.Writer, c.Request)
}
}
func (g *Gateway) matchRoute(path string) string {
for prefix, backend := range g.routes {
if strings.HasPrefix(path, prefix) {
return backend
}
}
return ""
}
// 使用
func main() {
g := New()
r := gin.New()
r.Use(gin.Recovery())
// 鉴权中间件
r.Use(authMiddleware())
// 所有请求转发到后端
r.NoRoute(g.Proxy())
r.Run(":8080")
}5.2 动态路由(结合服务发现)
go
type DynamicGateway struct {
registry *ConsulRegistry
cache sync.Map // path -> []*backend
}
func (g *DynamicGateway) Proxy() gin.HandlerFunc {
return func(c *gin.Context) {
serviceName := g.matchService(c.Request.URL.Path)
if serviceName == "" {
c.JSON(404, gin.H{"err": "no service"})
return
}
// 从服务发现获取实例列表
instances, err := g.registry.Discover(serviceName)
if err != nil || len(instances) == 0 {
c.JSON(503, gin.H{"err": "no instance"})
return
}
// 简单轮询负载均衡
target := instances[0].Address + ":" + strconv.Itoa(instances[0].Port)
// ... 创建 proxy 转发
}
}更完善的负载均衡可用 grpc.NewClient 配合 roundrobin resolver。
六、配置管理
6.1 Viper 集成
Viper 支持多源配置:环境变量、文件、命令行、远程配置中心。
go
package config
import (
"fmt"
"strings"
"github.com/spf13/viper"
)
type Config struct {
App AppConfig `mapstructure:"app"`
DB DBConfig `mapstructure:"db"`
Redis RedisConfig `mapstructure:"redis"`
Log LogConfig `mapstructure:"log"`
Services ServicesConfig `mapstructure:"services"`
}
type AppConfig struct {
Name string `mapstructure:"name"`
Port int `mapstructure:"port"`
Mode string `mapstructure:"mode"`
}
type DBConfig struct {
Driver string `mapstructure:"driver"`
DSN string `mapstructure:"dsn"`
MaxOpenConns int `mapstructure:"max_open_conns"`
MaxIdleConns int `mapstructure:"max_idle_conns"`
ConnMaxLifetime int `mapstructure:"conn_max_lifetime"` // seconds
}
type RedisConfig struct {
Addr string `mapstructure:"addr"`
Password string `mapstructure:"password"`
DB int `mapstructure:"db"`
PoolSize int `mapstructure:"pool_size"`
}
type LogConfig struct {
Level string `mapstructure:"level"`
Format string `mapstructure:"format"` // json / console
Output string `mapstructure:"output"` // stdout / file
}
type ServicesConfig struct {
User string `mapstructure:"user"`
Order string `mapstructure:"order"`
Goods string `mapstructure:"goods"`
}
func Load(env string) (*Config, error) {
v := viper.New()
// 默认值
v.SetDefault("app.port", 8080)
v.SetDefault("app.mode", "release")
v.SetDefault("db.max_open_conns", 100)
v.SetDefault("redis.pool_size", 100)
// 配置文件
v.SetConfigName(fmt.Sprintf("config.%s", env))
v.SetConfigType("yaml")
v.AddConfigPath("./configs")
v.AddConfigPath("/app/configs")
v.AddConfigPath(".")
if err := v.ReadInConfig(); err != nil {
return nil, fmt.Errorf("read config: %w", err)
}
// 环境变量覆盖(最高优先级)
v.AutomaticEnv()
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
// 例如 DB_DSN 覆盖 db.dsn
var cfg Config
if err := v.Unmarshal(&cfg); err != nil {
return nil, fmt.Errorf("unmarshal: %w", err)
}
return &cfg, nil
}6.2 多环境配置
configs/
├── config.dev.yaml # 开发环境
├── config.test.yaml # 测试环境
├── config.staging.yaml # 预发环境
└── config.prod.yaml # 生产环境yaml
# config.prod.yaml
app:
name: user-service
port: 8080
mode: release
db:
driver: mysql
dsn: "user:pass@tcp(mysql.prod:3306)/user?parseTime=true&loc=Local"
max_open_conns: 200
max_idle_conns: 50
conn_max_lifetime: 300
redis:
addr: "redis.prod:6379"
password: "${REDIS_PASSWORD}" # 从环境变量注入
db: 0
pool_size: 200
log:
level: info
format: json
output: stdout
services:
user: "user-service:8080"
order: "order-service:8080"
goods: "goods-service:8080"启动时通过环境变量切换:
go
func main() {
env := os.Getenv("APP_ENV")
if env == "" {
env = "dev"
}
cfg, err := config.Load(env)
if err != nil {
log.Fatal(err)
}
// ... 使用 cfg
}6.3 配置热更新
Viper 支持监听配置文件变化:
go
v.OnConfigChange(func(e fsnotify.Event) {
log.Printf("config changed: %s", e.Name)
var newCfg Config
if err := v.Unmarshal(&newCfg); err != nil {
log.Printf("reload config: %v", err)
return
}
// 应用新配置(注意并发安全)
applyConfig(newCfg)
})
v.WatchConfig()七、完整微服务示例
7.1 项目结构
user-service/
├── cmd/
│ └── server/
│ └── main.go
├── internal/
│ ├── handler/ # HTTP/gRPC handler
│ ├── service/ # 业务逻辑
│ ├── repository/ # 数据访问
│ └── model/ # 数据模型
├── proto/ # protobuf 定义
├── configs/ # 配置文件
├── Dockerfile
├── docker-compose.yml
└── go.mod7.2 主函数
go
package main
import (
"context"
"log"
"net"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"google.golang.org/grpc"
"github.com/gin-gonic/gin"
)
func main() {
// 加载配置
env := os.Getenv("APP_ENV")
cfg, err := config.Load(env)
if err != nil {
log.Fatal(err)
}
// 初始化依赖
db := initDB(cfg.DB)
rdb := initRedis(cfg.Redis)
logger := initLogger(cfg.Log)
// 服务注册
reg, _ := registry.NewConsul(cfg.Consul.Addr)
ip := getOutboundIP()
svcID := fmt.Sprintf("%s-%s-%d", cfg.App.Name, ip, cfg.App.Port)
_ = reg.Register(registry.ServiceInfo{
ID: svcID,
Name: cfg.App.Name,
Address: ip,
Port: cfg.App.Port,
Check: &api.AgentServiceCheck{
HTTP: fmt.Sprintf("http://%s:%d/health", ip, cfg.App.Port),
Interval: "10s",
},
})
defer reg.Deregister(svcID)
// 启动 gRPC
go startGRPC(cfg.GRPC.Port, db)
// 启动 HTTP
httpServer := startHTTP(cfg, db, rdb, logger)
// 优雅关停
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
logger.Info("shutting down...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
httpServer.Shutdown(ctx)
}
func startHTTP(cfg *config.Config, db *sql.DB, rdb *redis.Client, logger *zap.Logger) *http.Server {
gin.SetMode(cfg.App.Mode)
r := gin.New()
r.Use(gin.Recovery(), zapMiddleware(logger))
// 健康检查
hh := NewHealthHandler()
hh.Add("db", &DBChecker{db: db})
r.GET("/health", hh.Liveness)
r.GET("/ready", hh.Readiness)
// 业务路由
userRepo := repository.NewUserRepo(db)
userSvc := service.NewUserService(userRepo, rdb)
userHandler := handler.NewUserHandler(userSvc)
api := r.Group("/api/v1")
api.GET("/users/:id", userHandler.GetUser)
api.POST("/users", userHandler.CreateUser)
server := &http.Server{
Addr: fmt.Sprintf(":%d", cfg.App.Port),
Handler: r,
}
go func() {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}()
return server
}八、常见陷阱与最佳实践
8.1 容器中的信号处理
容器停止时,Docker 发送 SIGTERM,Go 程序默认会立即退出。需要显式捕获并优雅关停:
go
// ❌ 错误:r.Run() 阻塞,无法处理信号
r.Run()
// ✅ 正确:用 http.Server 并手动管理生命周期
server := &http.Server{Addr: ":8080", Handler: r}
go server.ListenAndServe()
<-quit
server.Shutdown(ctx)8.2 容器时区
dockerfile
# ❌ 容器默认 UTC,日志时间错误
FROM alpine
# ✅ 设置时区
RUN apk add --no-cache tzdata && \
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
echo "Asia/Shanghai" > /etc/timezone8.3 gRPC 与 HTTP 的选择
- 对外 API:HTTP/RESTful(前端、第三方易用)
- 内部高频调用:gRPC(性能高、契约严格)
- 流式数据:gRPC stream(实时推拉)
不要一刀切用 gRPC,对外接口还是 HTTP 更友好。
8.4 服务发现的容量
Consul/Etcd 的服务发现查询频率要控制,频繁查询会拖垮注册中心。建议:
- 客户端缓存实例列表,定期刷新(5-10s)
- 用 watch 而非轮询(etcd 原生支持)
- 大规模集群用 SDK 的连接池(grpc.ClientConn 自带负载均衡)
九、小结
本文系统讲解了 Gin 在微服务架构下的部署与集成:
- Docker 化:多阶段构建、scratch/alpine 镜像优化、docker-compose 编排。
- 微服务角色:Gin 作为 HTTP API 层,gRPC 作为内部 RPC 层,二者可共存。
- gRPC 集成:双端口服务、gRPC-Gateway 自动生成 RESTful API。
- 服务发现:Consul/Etcd 注册与发现,健康检查端点(Liveness/Readiness 分离)。
- API 网关:用
httputil.ReverseProxy实现轻量级网关,结合服务发现做动态路由。 - 配置管理:Viper 多源配置、多环境分离、热更新。
下一篇我们将进入生产部署与架构最佳实践,讲解优雅关停、安全、监控与项目结构规范。