Skip to content

Go Package Design

Phase 02 — Go Core 涵盖:package design


1. 学习目标

完成本知识点后,你应该能够:

  • 理解 Go 包(package)是编译和复用的基本单元
  • 掌握导出规则:首字母大写 = 公开,小写 = 包内私有
  • 组织合理的项目目录结构(cmd、internal、pkg)
  • 使用 internal 目录限制包的导入范围
  • 避免循环依赖,设计清晰的包边界
  • 为数字孪生后端项目规划可维护的模块结构

2. 为什么需要

当代码从单个 main.go 增长到数百行,全部堆在一个文件里会变得难以维护。Go 通过 package 强制模块化:每个目录一个包,通过导入路径组合。

合理的 package design 让:

  • 业务逻辑与 HTTP 层分离(后续换 Gin 不影响 core)
  • 测试可以单独测试 internal/service 而不启动 HTTP 服务器
  • 团队成员并行开发不同包,减少冲突

3. 核心概念

3.1 一个目录一个包

myproject/
├── go.mod
├── cmd/
│   └── server/
│       └── main.go      // package main
└── internal/
    └── device/
        └── agv.go       // package device

main.go 中:import "myproject/internal/device"

3.2 导出规则

标识符首字母可见性
大写包外可访问(exported)
小写仅包内可访问(unexported)

3.3 internal 目录

Go 编译器强制:internal/ 下的包只能被父目录及子目录导入,外部模块无法 import。用于保护实现细节。

3.4 常见布局

目录用途
cmd/可执行入口,每个子目录一个 main
internal/私有应用逻辑,外部不可 import
pkg/可选,可被外部项目复用的库代码
api/OpenAPI 定义、proto 文件等

3.5 避免循环依赖

包 A import B,B 不能 import A。解决:抽取公共 interface 到第三个包,或合并过小的包。


4. 基础语法

示例项目结构见 workspace/phase-02/package-design/

internal/device/agv.go

go
package device

import "fmt"

// AGV 导出类型
type AGV struct {
    ID    string
    speed float64 // 小写:包外不可直接访问
}

func NewAGV(id string, speed float64) *AGV {
    return &AGV{ID: id, speed: speed}
}

func (a *AGV) Speed() float64 { return a.speed }

func (a *AGV) Report() string {
    return fmt.Sprintf("%s @ %.1f m/s", a.ID, a.speed)
}

cmd/server/main.go

go
package main

import (
    "fmt"
    "myproject/internal/device"
)

func main() {
    agv := device.NewAGV("AGV-001", 1.2)
    fmt.Println(agv.Report())
    // agv.speed = 2.0  // ❌ 编译错误:speed 未导出
}

go.mod

go
module myproject

go 1.22

5. 代码解析

go
type AGV struct {
    ID    string  // 导出
    speed float64 // 未导出
}

通过导出规则封装内部状态。包外只能通过 NewAGVSpeed() 访问,类似 TS 的 private 字段 + getter。

go
func NewAGV(...) *AGV

构造函数模式:Go 没有 constructor 关键字,用 NewXxx 函数初始化,可在此做校验。

go
internal/device/

internal 路径下的包,模块外部无法 import,防止 API 消费者依赖内部实现。


6. JavaScript / TypeScript 对比

概念GoJavaScript / TypeScript
模块单元package(目录)file(ES module)
可见性首字母大小写export / private
私有实现unexported + internalmodule scope
路径module path(go.mod)import path
循环依赖编译禁止运行时可能出问题
monorepo多 module 或多 packageworkspace / packages

关键差异

  1. Go 可见性是包级别,不是文件级别;同包所有文件共享可见性
  2. internal 是编译器 enforced,TS 的 private 只是编译期检查
  3. Go 一个目录不能有两个 package(测试 _test 除外)

7. 常见错误

错误 1:package 名与目录名不一致

go
// 目录 device/,文件写 package dev  // ❌ 惯例要求一致
package device  // ✅

错误 2:循环 import

handler → service → handler  // ❌ 编译失败

错误 3:过度拆分

每个 struct 一个 package 会导致 import 爆炸。按职责而非按类型拆分。

错误 4:滥用 pkg/

pkg/ 意味「给外部用的库」。应用私有代码放 internal/

错误 5:main 包包含业务逻辑

go
// ❌ 全部写在 cmd/server/main.go
// ✅ main 只做组装:读配置、启动 server、依赖注入

8. 实际应用

数字孪生后端推荐结构

twin-server/
├── go.mod
├── cmd/
│   └── api/
│       └── main.go           # 启动 HTTP
├── internal/
│   ├── device/               # AGV、Sensor 领域模型
│   ├── service/              # 业务逻辑
│   ├── handler/              # HTTP handlers
│   └── store/                # 内存/DB 存储
└── api/
    └── openapi.yaml          # 可选 API 文档

Phase 03 REST API 项目可从此结构起步,Phase 04 接入 PostgreSQL 时只需扩展 store/


9. 深入理解

9.1 包初始化 init()

每个包可有 func init(),在 import 时自动执行。避免复杂逻辑和隐式副作用。

9.2 接口定义的位置

  • 接口在使用方包定义(accept interfaces)
  • 实现在提供方包(return structs)

9.3 命名

  • package 名简短小写单数:device 不是 devices
  • 避免 stutter:device.Device OK,device.DeviceManager 可考虑 device.Manager

9.4 go mod 与 import path

import path = module名/相对路径。移动目录需同步更新 import。


10. 练习

详细题目见 exercises/phase-02-core/04-package-design.md

Level 1 — 基础

练习 1.1:创建 internal/device 包,导出 AGV struct 和 NewAGV。

练习 1.2:在 main 中 import 并使用,验证小写字段不可访问。

练习 1.3:同包多文件:types.go + agv.go。

Level 2 — 应用

练习 2.1:添加 internal/sensor 包,实现 Sensor 类型。

练习 2.2:在 internal/registry 中管理 device 和 sensor。

练习 2.3:用 interface 在 registry 中统一存储 DeviceReporter。

Level 3 — 综合

练习 3.1:拆分 handler 层(仅打印)和 service 层(业务逻辑)。

练习 3.2:设计 store 包 interface + 内存实现。

Level 4 — 项目实践

练习 4.1:在 workspace/phase-02/package-design/ 搭建 mini twin-server 骨架:cmd + internal 三层结构,可 go run ./cmd/server


11. 学习检查

  1. Go 如何控制符号的可见性?
  2. internal 目录的特殊规则是什么?
  3. 为什么不能循环 import?如何解决?
  4. cmd/internal/ 通常放什么?
  5. package 名与目录名为什么要一致?
  6. 面向 interface 编程时,interface 应定义在哪一侧?

12. 下一步

已完成下一知识点关系
package designgoroutine / channel并发代码也需合理分包
Phase 03 HTTPhandler 包是 Web 层的核心

建议进入并发专题,之后 Phase 03 会用 package design 组织 REST API 项目。


学习导航

上一篇:Go JSON · 对应练习 · 下一篇:Goroutine、Channel、Select