Cairn
← 返回博客

从空目录到能跑:Go 博客骨架搭建实录

从空目录到能跑:Go 博客骨架搭建实录

项目第一阶段我只定了一个验收标准:

curl http://localhost:8080/healthz

能稳定返回 200,并且修改代码后服务自动重启。

听起来有点寒酸,但这个阶段真正要验证的不是业务,而是开发循环:

改代码 -> 编译 -> 重启 -> 发请求 -> 看日志

如果这条链路不顺,后面每加一个功能都要交一次重复劳动税。

一、目录先按业务分,不按技术堆

最早的目录是传统三层:

internal/
├── handler/
├── service/
└── repository/

功能少时很清楚,功能多后会变成:

handler 里放所有 HTTP
service 里放所有业务
repository 里放所有 SQL

找一段评论逻辑需要横跨三个大目录。后来改成按业务域组织:

internal/
├── app/
├── auth/
│   ├── handler/
│   ├── service/
│   └── repository/
├── blog/
│   ├── handler/
│   ├── service/
│   └── repository/
├── tool/
├── config/
└── web/

三层仍然存在,但被限制在各自模块里。新增作品集时,只需要增加一个 internal/project,而不是继续往三个公共抽屉里塞东西。

二、配置加载:YAML 描述结构,环境变量保存秘密

开发配置:

environment: development

server:
  host: 127.0.0.1
  port: 8080

database:
  host: localhost
  port: 5432
  user: cairn
  password: ${DB_PASSWORD}
  name: cairn
  sslmode: disable

对应结构:

type Config struct {
    Environment string         `mapstructure:"environment"`
    Server      ServerConfig   `mapstructure:"server"`
    Database    DatabaseConfig `mapstructure:"database"`
}

这里踩了第一个坑:Viper 的 AutomaticEnv() 不会自动展开 YAML 字符串中的 ${DB_PASSWORD}

错误现象是数据库认证失败,但配置文件看起来完全正确。最后发现传给 PostgreSQL 的密码真的是字面量:

${DB_PASSWORD}

解决方式:

cfg.Database.Password = os.ExpandEnv(cfg.Database.Password)

加载后还要做校验:

if cfg.Database.Password == "" {
    return nil, errors.New("database.password is required")
}

配置错误应该在启动阶段直接失败,而不是等第一个用户访问时再给他一张 500 页面作为欢迎礼物。

三、PostgreSQL 用 Docker,Go 不用

本地数据库配置:

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: cairn
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: cairn
    ports:
      - "127.0.0.1:5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data

数据库放容器里有几个好处:

  • 不污染宿主机
  • 版本固定
  • 数据卷可控
  • 重置环境方便

Go 应用直接运行在宿主机,因为 Air 监听文件、重新编译和传递信号都更简单。

Air -> Go -> PostgreSQL 容器

本地开发和生产部署不必在每个细节上完全一致。需要一致的是行为和依赖版本,不是连敲命令的姿势都相同。

四、应用入口只负责生命周期

入口文件不应该知道每个 repository 怎么创建。它只负责:

  1. 初始化日志。
  2. 创建应用。
  3. 启动 HTTP Server。
  4. 监听退出信号。
  5. 优雅关闭。
func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    application, err := app.New(context.Background(), logger)
    if err != nil {
        logger.Error("initialize app", "error", err)
        os.Exit(1)
    }
    defer application.Close()

    srv := &http.Server{
        Addr:         "127.0.0.1:8080",
        Handler:      application.Router,
        ReadTimeout:  15 * time.Second,
        WriteTimeout: 15 * time.Second,
        IdleTimeout:  60 * time.Second,
    }

    if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
        logger.Error("server stopped", "error", err)
    }
}

Repository、Service、Handler 的组装放在 internal/app。这样入口不会随着功能增加变成一份两百行的零件清单。

五、数据库连接为什么要 Ping

pgxpool.New 成功不代表数据库真的能访问。它主要创建连接池配置,连接可能在第一次查询时才建立。

因此启动阶段需要:

db, err := pgxpool.New(ctx, cfg.Database.DSN())
if err != nil {
    return nil, err
}

if err := db.Ping(ctx); err != nil {
    db.Close()
    return nil, err
}

如果密码错误、端口没开或容器没启动,应用应该立即退出,让 systemd 或开发者看到明确错误。

“先启动,等有人访问再爆炸”不算容错,只算延迟公布坏消息。

六、健康检查要简单

健康检查:

func healthz(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    _, _ = w.Write([]byte(`{"status":"ok","version":"dev"}`))
}

它的用途:

  • 手工确认服务存活
  • 反向代理探测
  • 部署后验证
  • 监控平台定时请求

健康接口不要渲染模板,也不要做昂贵查询。它应该像敲门:快速告诉外面“服务还活着”。

七、接入 Air 热重载

安装:

go install github.com/air-verse/air@latest

启动:

DB_PASSWORD=本地密码 air

Air 会监听 Go 文件,重新构建并启动临时二进制。

常见问题一:命令找不到。

go env GOPATH

go install 默认把工具放进:

$(go env GOPATH)/bin

把这个目录加入 PATH,或者直接使用完整路径。

常见问题二:旧版配置字段失效。工具升级后如果出现 deprecated 警告,不要假装日志没写。警告通常只是 Bug 的预告片。

八、国内网络下的依赖下载

Go 模块下载超时时:

GOPROXY=https://goproxy.cn,direct go mod tidy

Docker 镜像无法拉取时,应先确认是仓库网络问题,而不是反复修改 Compose 文件。

排查顺序:

docker info
docker pull postgres:16
docker compose config
docker compose up -d
docker compose ps

把“配置解析失败”“镜像下载失败”“容器启动失败”分开看,会少走很多弯路。

九、最终验证

启动数据库:

docker compose up -d

启动应用:

DB_PASSWORD=本地密码 air

验证:

curl -i http://localhost:8080/healthz

确认:

  • HTTP 状态是 200
  • 日志为结构化 JSON
  • 修改 handler 后 Air 会自动重启
  • 停止 PostgreSQL 后应用启动会明确失败
  • Ctrl+C 能触发优雅关闭

这个阶段没有文章,没有用户,也没有漂亮页面。但它建立了一条可信的开发链路。

项目后面能跑多远,往往取决于最开始这条链路有多无聊、多稳定。

评论

暂无评论,来说点什么吧。

请先登录后再发表评论

去登录