标签: Go

  • Go项目架构实战:从零搭建生产级API服务

    Go项目架构实战:从零搭建生产级API服务

    为什么项目结构很重要

    代码结构是团队协作的隐形成本。好的结构让人一眼看懂模块边界,差的结构让改一个 bug 需要改七个文件。随着项目增长,重新组织结构的代价会越来越高——所以从第一天就把结构做好。

    一种实用的 Go 项目分层

    myapp/
    ├── cmd/
    │   └── server/
    │       └── main.go          # 入口,只负责 wire 和启动
    ├── internal/
    │   ├── handler/             # HTTP/gRPC handler(接收请求)
    │   ├── service/              # 业务逻辑层(纯函数,无依赖注入简化测试)
    │   ├── repo/                 # 数据访问层(DB / Cache / 外部 API)
    │   └── model/                # 数据模型(贫血模型,纯数据结构)
    ├── pkg/
    │   └── response/             # 对外暴露的公共库
    ├── migrations/              # 数据库迁移文件
    ├── config/
    │   └── config.go             # 配置加载
    ├── Makefile
    └── go.mod

    各层职责定义

    Handler 层负责解析请求参数、做基础校验、调用 Service 层、组装响应。注意:Handler 不写任何业务逻辑。

    Service 层包含核心业务逻辑。推荐用纯函数风格,依赖通过参数传入,方便写单元测试。

    Repo 层封装所有数据访问,只对 Service 层暴露存储抽象——换数据库、换缓存策略,不影响上层代码。

    避免几个常见反模式

    • 把 model 当贫血模型塞进 repo 层,导致 repo 越来越胖
    • 在 handler 里直接写 SQL,耦合数据库细节
    • 全局变量(var db *sql.DB)跨包共享,测试时无法 mock
    • 把所有类型都放在 model 包,没有按领域聚合

    小结

    Go 的项目结构没有标准答案,但有一个原则:让每一层都专注于一件事。如果你的 handler 里有 SQL,你的 service 里在 new 各种客户端,那是结构在提醒你需要调整了。

    来源:https://www.dnote.cn

  • Go 并发编程:Goroutine 与 Channel 实战指南

    Go 并发编程:Goroutine 与 Channel 实战指南

    为什么 Go 的并发模型值得专门学

    Go 的并发哲学来自 C.A.R. Hoare 的 CSP 理论,与传统线程模型完全不同。理解这套模型,是写出高性能 Go 服务的基础。

    Goroutine 到底是什么

    goroutine 是由 Go 运行时管理的轻量级线程,创建成本极低(约 2KB 栈空间),可以轻松创建数万个 goroutine 而不耗尽系统资源。与 OS 线程的 1:1 模型不同,Go 使用 M:N 调度——M 个 OS 线程上运行 N 个 goroutine,由 Go 运行时 GOMAXPROCS 控制并行度。

    一个简单的 goroutine 示例

    // 启动一个 goroutine
    go func() {
        fmt.Println("running in goroutine")
    }()
    
    // 主 goroutine 继续执行,不等待子 goroutine
    fmt.Println("main exits")

    Channel:goroutine 之间的通信桥梁

    channel 是 goroutine 间传递数据的管道,有带缓冲(buffered)和不带缓冲(unbuffered)两种。推荐优先使用无缓冲 channel,这样发送与接收必须配对,强制调用方思考同步问题,避免隐藏的竞态。

    ch := make(chan int)         // 无缓冲
    ch <- 42                       // 发送(阻塞直到有人接收)
    value := <-ch                  // 接收
    
    // 带缓冲 channel(队列)
    bufCh := make(chan int, 100)  // 最多缓存 100 条,不阻塞

    select:多路复用的并发协调

    select 关键字让一个 goroutine 可以同时监听多个 channel,就跟 select 系统调用监听多个 fd 一样。配合 default case 可以实现非阻塞发送/接收。

    select {
    case msg := <-ch1:
        fmt.Println("received from ch1:", msg)
    case msg := <-ch2:
        fmt.Println("received from ch2:", msg)
    case <-time.After(time.Second):
        fmt.Println("timeout")
    default:
        fmt.Println("no message available")
    }

    常见踩坑与最佳实践

    • 不要用 channel 传递所有数据——共享内存(sync.Mutex/sync.Map)有时更简单
    • 永远不要关闭由别人发送的 channel,避免 panic
    • 使用 context.Context 来控制 goroutine 的生命周期,而不是用 channel 硬传退出信号
    • 在生产环境加上 -race 参数跑单元测试,检查数据竞争

    总结

    Go 的并发模型用起来简单,但要真正用好,需要理解 goroutine 的调度原理、channel 的阻塞语义,以及 context 在多层调用中的传递方式。掌握这些,你就能写出既高效又安全的并发代码。

    来源:https://www.dnote.cn

  • Go部署:Docker/K8s实战配置

    Go部署:Docker/K8s实战配置

    多阶段构建:让镜像小到 10MB 以内

    Go 是静态编译语言,只需要编译后的二进制文件,不需要运行时环境。多阶段构建可以分离构建环境和运行环境,最终镜像只包含二进制文件。

    # 阶段1:构建
    FROM golang:1.23-alpine AS builder
    WORKDIR /app
    COPY go.mod go.sum ./
    RUN go mod download
    COPY . .
    RUN CGO_ENABLED=0 GOOS=linux go build -ldflags='-w -s' -o server .
    
    # 阶段2:运行
    FROM alpine:3.19
    RUN addgroup -S appgroup && adduser -S appuser -G appgroup
    WORKDIR /app
    COPY --from=builder /app/server .
    RUN chmod +x server
    USER appuser
    CMD ["./server"]

    利用 Docker BuildKit 加速构建

    # 启用 BuildKit
    export DOCKER_BUILDKIT=1
    
    # 并行下载依赖,大幅加速
    docker build --progress=plain -t myapp:latest .

    健康检查:让 K8s/Compose 知道服务已就绪

    在 Dockerfile 里加上 HEALTHCHECK,让 orchestrator 在服务真正能接收请求时才将其加入负载均衡。

    HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
      CMD wget -qO- http://localhost:8080/health || exit 1

    环境变量注入的坑

    Go 程序默认会在 ENV 改变时重新加载,但 Docker 层会缓存 ENV。用 ENTRYPOINT 脚本或 Viper 动态读取,比硬编码 ENV 常量更灵活。

    小结

    Docker 化 Go 服务的核心是:镜像要小(多阶段构建)、构建要快(BuildKit)、健康要可感知。做到这三点,生产部署就没什么大问题了。

    来源:https://www.dnote.cn

  • Go ORM对比:GORM vs Ent vs Kitex

    Go ORM对比:GORM vs Ent vs Kitex

    为什么 Go 生态里有这么多 ORM 选择

    Go 生态偏好显式而非隐式,所以没有 Java Hibernate 那样的大一统 ORM。GORM、Ent 和 Kitex(配合 codegen)是三条不同的路,各有适用场景。

    GORM:最成熟,上手最快

    GORM 是事实标准,生态丰富、文档完善。缺点是隐式 SQL、链式 API 容易写出难以维护的代码,且性能一般。适合快速开发或中小型项目。

    Ent:代码生成,类型安全

    Ent 是 Facebook 出品的代码生成式 ORM。Schema 定义后自动生成代码,类型安全、IDE 友好、支持图结构查询。缺点是学习曲线稍陡,新增字段需要重新生成。

    Kitex + SQLBricks:微服务场景的最佳组合

    字节跳动的 Kitex 框架不绑定 ORM,推荐配合 sqlx 或 sqlc 使用。sqlc 可以从 SQL 语句生成类型安全的 Go 代码,彻底避免 SQL 注入,同时保持 SQL 的可读性。

    各场景推荐

    场景 推荐 原因
    快速原型/MVP GORM 上手快,生态成熟
    中大型项目 Ent 类型安全,可维护
    高性能微服务 Kitex + sqlx 零抽象,性能可控
    需要图查询 Ent 原生支持图结构

    小结

    选 ORM 就是选抽象程度:GORM 是高抽象(牺牲性能和可维护性),Ent 是中抽象(平衡),sqlx 是低抽象(保留 SQL 的所有优点)。没有最优解,只有最适合当前项目规模和团队风格的方案。

    来源:https://www.dnote.cn

  • Go并发编程精讲:goroutine/channel原理与避坑指南

    Go并发编程精讲:goroutine/channel原理与避坑指南

    深入理解 GMP 调度模型

    Go 运行时调度器将 goroutine 映射到 OS 线程上执行,GMP 代表 G(Goroutine)、M(Machine/OS 线程)、P(Processor,执行上下文)。P 的数量默认等于 CPU 核心数,可以通过 GOMAXPROCS 调整,这是 Go 并发性能的关键参数。

    goroutine 的生命周期与状态

    goroutine 有多个状态:waiting(阻塞在 channel 或系统调用)、runnable(就绪等待调度)、running(正在执行)。理解这些状态有助于分析死锁和性能问题。

    channel 的底层实现

    channel 内部有环形队列和两个等待队列(sendq 和 recvq)。向已满的无缓冲 channel 发送会阻塞,goroutine 被挂在 sendq 上;接收时会从 recvq 唤醒一个 sender 或从队列取数据。

    常见并发陷阱

    • 向已关闭的 channel 发送数据会 panic,但接收会返回零值
    • 多个 goroutine 同时等待同一个 channel,只有 1 个会被唤醒——用 sync.WaitGroup 而非 channel 控制退出
    • for range channel 会一直阻塞直到 channel 关闭
    • context.WithCancel 是优雅退出多个 goroutine 的标准方式

    小结

    理解 GMP 调度和 channel 底层原理,才能真正写出高效的并发代码。遇到性能问题时,用 runtime.NumGoroutine() 和 pprof 分析 goroutine 的状态分布是第一步。

    来源:https://www.dnote.cn

  • Docker 容器化 Go 服务的几个实战细节

    Docker 容器化 Go 服务的几个实战细节

    多阶段构建:让镜像小到 10MB 以内

    Go 是静态编译语言,只需要编译后的二进制文件,不需要运行时环境。多阶段构建可以分离构建环境和运行环境,最终镜像只包含二进制文件。

    # 阶段1:构建
    FROM golang:1.23-alpine AS builder
    WORKDIR /app
    COPY go.mod go.sum ./
    RUN go mod download
    COPY . .
    RUN CGO_ENABLED=0 GOOS=linux go build -ldflags='-w -s' -o server .
    
    # 阶段2:运行
    FROM alpine:3.19
    RUN addgroup -S appgroup && adduser -S appuser -G appgroup
    WORKDIR /app
    COPY --from=builder /app/server .
    RUN chmod +x server
    USER appuser
    CMD ["./server"]

    利用 Docker BuildKit 加速构建

    # 启用 BuildKit
    export DOCKER_BUILDKIT=1
    
    # 并行下载依赖,大幅加速
    docker build --progress=plain -t myapp:latest .

    健康检查:让 K8s/Compose 知道服务已就绪

    在 Dockerfile 里加上 HEALTHCHECK,让 orchestrator 在服务真正能接收请求时才将其加入负载均衡。

    HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
      CMD wget -qO- http://localhost:8080/health || exit 1

    环境变量注入的坑

    Go 程序默认会在 ENV 改变时重新加载,但 Docker 层会缓存 ENV。用 ENTRYPOINT 脚本或 Viper 动态读取,比硬编码 ENV 常量更灵活。

    小结

    Docker 化 Go 服务的核心是:镜像要小(多阶段构建)、构建要快(BuildKit)、健康要可感知。做到这三点,生产部署就没什么大问题了。

    来源:https://www.dnote.cn

  • Go 并发编程:Goroutine 与 Channel 实战指南

    为什么 Go 的并发模型值得专门学

    Go 的并发哲学来自 C.A.R. Hoare 的 CSP 理论,与传统线程模型完全不同。理解这套模型,是写出高性能 Go 服务的基础。

    Goroutine 到底是什么

    goroutine 是由 Go 运行时管理的轻量级线程,创建成本极低(约 2KB 栈空间),可以轻松创建数万个 goroutine 而不耗尽系统资源。与 OS 线程的 1:1 模型不同,Go 使用 M:N 调度——M 个 OS 线程上运行 N 个 goroutine,由 Go 运行时 GOMAXPROCS 控制并行度。

    一个简单的 goroutine 示例

    // 启动一个 goroutine
    go func() {
        fmt.Println("running in goroutine")
    }()
    
    // 主 goroutine 继续执行,不等待子 goroutine
    fmt.Println("main exits")

    Channel:goroutine 之间的通信桥梁

    channel 是 goroutine 间传递数据的管道,有带缓冲(buffered)和不带缓冲(unbuffered)两种。推荐优先使用无缓冲 channel,这样发送与接收必须配对,强制调用方思考同步问题,避免隐藏的竞态。

    ch := make(chan int)         // 无缓冲
    ch <- 42                       // 发送(阻塞直到有人接收)
    value := <-ch                  // 接收
    
    // 带缓冲 channel(队列)
    bufCh := make(chan int, 100)  // 最多缓存 100 条,不阻塞

    select:多路复用的并发协调

    select 关键字让一个 goroutine 可以同时监听多个 channel,就跟 select 系统调用监听多个 fd 一样。配合 default case 可以实现非阻塞发送/接收。

    select {
    case msg := <-ch1:
        fmt.Println("received from ch1:", msg)
    case msg := <-ch2:
        fmt.Println("received from ch2:", msg)
    case <-time.After(time.Second):
        fmt.Println("timeout")
    default:
        fmt.Println("no message available")
    }

    常见踩坑与最佳实践

    • 不要用 channel 传递所有数据——共享内存(sync.Mutex/sync.Map)有时更简单
    • 永远不要关闭由别人发送的 channel,避免 panic
    • 使用 context.Context 来控制 goroutine 的生命周期,而不是用 channel 硬传退出信号
    • 在生产环境加上 -race 参数跑单元测试,检查数据竞争

    总结

    Go 的并发模型用起来简单,但要真正用好,需要理解 goroutine 的调度原理、channel 的阻塞语义,以及 context 在多层调用中的传递方式。掌握这些,你就能写出既高效又安全的并发代码。

    来源:dnote.cn

  • Go语言获取指定年份生肖

    Go语言获取指定年份生肖

    根据给定年份,返回生肖字符串,公元前使用负值即可。(比如2022年,调用使用GetShengXiao(2022),公元前21年,调用使用GetShengXiao(-21))。
    
    
    // 获取生肖索引
    func GetShengXiaoIndex(year int) int {
        // 不存在0年
        if year == 0 {
            panic("err: invalid year")
        }
    
        // 公元前补1
        if year < 0 {
            year += 1
        }
    
        idx := (year - 4) % 12
    
        if idx < 0 {
            idx += 12
        }
    
        return idx
    }
    
    // 根据给定年份获取生肖
    func GetShengXiao(idx int) string {
        return [12]string{"鼠", "牛", "虎", "兔", "龙", "蛇", "马", "羊", "猴", "鸡", "狗", "猪"}[idx]
    }
    
  • MySQL给已存在的主键字段添加自增AUTO_INCREMENT

    MySQL给已存在的主键字段添加自增AUTO_INCREMENT

    每次都记不起来,记录一下…

    # 添加自增约束
    alter table table_name modify column COLUMN_NAME COLUMN_TYPE auto_increment;
    # 配置自增起始值
    alter table table_name auto_increment=10000;
    
  • Go语言获取指定年份生肖

    Go语言获取指定年份生肖

    根据给定年份,返回生肖字符串,公元前使用负值即可。(比如2022年,调用使用GetShengXiao(2022),公元前21年,调用使用GetShengXiao(-21))。

    (更多…)