Golang 使用决策表
一、先给你总原则
一句话原则
- 无状态逻辑:优先
function - 有状态行为:优先
method - 有依赖要聚合:用
struct + method - 需要替换/解耦:由调用方定义
interface - 需要保证初始化正确:提供
NewXxx
二、核心决策表
| 场景 | 推荐写法 | 为什么 | 例子 |
|---|---|---|---|
| 纯计算、纯转换、纯校验 | function |
简单、直接、低成本 | Parse(), Validate(), Format() |
| 逻辑天然属于某个数据 | method |
行为和数据绑定更清晰 | user.FullName() |
| 要修改对象内部状态 | pointer method |
需要修改接收者 | order.MarkPaid() |
| 只读取对象状态,不修改 | value method 或 pointer method |
小对象可 value,大对象常用 pointer 保持一致 | money.String() |
| 需要持有 repo/db/http client/logger | struct + method |
便于聚合依赖 | UserService.Create() |
| 只是为了“分类”函数 | package + function |
Go 用 package 做组织,不必强造对象 | password.Hash() |
| 需要多种实现替换 | interface |
面向使用方抽象 | type Repo interface { Save(...) } |
| 只有一个实现,且不会被替换 | 先不用 interface |
避免过早抽象 | 先写具体类型 |
| 初始化有前置条件 | NewXxx(...) |
保证对象创建后可用 | NewUserService(repo) |
| 初始化只是零值即可用 | 直接字面量/零值 | 不要为形式写构造器 | var buf bytes.Buffer |
| 工具函数集合 | 不要 Util struct |
多数是伪抽象 | 用包级函数 |
| 需要满足标准库接口 | 给类型加方法 | Go 接口靠方法实现 | ServeHTTP, String, Write |
三、你写代码时的实际判断流程
你可以每次按这个顺序判断。
1. 这段逻辑是否无状态?
如果是,先写 function
func NormalizeEmail(email string) string {
return strings.TrimSpace(strings.ToLower(email))
}
不要写成:
type EmailUtil struct{}
func (u *EmailUtil) Normalize(email string) string { ... }
2. 这段逻辑是否天然属于某个类型?
如果是,写 method
type User struct {
FirstName string
LastName string
}
func (u User) FullName() string {
return strings.TrimSpace(u.FirstName + " " + u.LastName)
}
因为 FullName 明显是 User 的行为。
3. 这段逻辑是否要修改状态?
如果要修改,优先 pointer receiver
type Account struct {
Balance int64
}
func (a *Account) Deposit(n int64) error {
if n <= 0 {
return errors.New("invalid amount")
}
a.Balance += n
return nil
}
4. 这段逻辑是否依赖外部资源?
如果依赖 DB、Redis、RPC、HTTP client、配置、logger,就用 struct + method
type UserService struct {
repo UserRepo
logger Logger
}
func (s *UserService) Create(ctx context.Context, req CreateUserReq) error {
// 编排业务流程
return nil
}
这里 struct 的核心价值不是 OO,而是聚合依赖。
5. 是否真的需要 interface?
只有在以下情况才优先考虑:
- 调用方需要解耦
- 单元测试需要替身
- 确实存在多实现
例如:
type UserRepo interface {
Save(ctx context.Context, user User) error
}
注意:这个接口通常应该由使用它的包定义,而不是实现方预先定义一个大接口。
6. 是否需要 NewXxx()?
需要时机:
func NewUserService(repo UserRepo, logger Logger) *UserService {
return &UserService{
repo: repo,
logger: logger,
}
}
适合 NewXxx 的情况:
- 要注入依赖
- 要校验参数
- 要初始化字段
- 要隐藏内部实现
不适合时:
- 零值已经可用
- 只是返回一个空 struct
- 只是为了“统一风格”
四、function / method / interface / New 的拆分表
1)什么时候用 function
| 判断项 | 用 function |
|---|---|
| 不依赖对象内部字段 | 是 |
| 不修改任何状态 | 是 |
| 更像算法、转换、校验 | 是 |
| 可以独立测试输入输出 | 是 |
| 放在 package 下更自然 | 是 |
典型例子:
func HashPassword(raw string) (string, error)
func ValidatePhone(phone string) error
func ParseJWT(token string) (*Claims, error)
func BuildOrderNo(ts time.Time, seq int64) string
function 适合的关键词
- Parse
- Format
- Validate
- Convert
- Build
- Merge
- Filter
- Map
- Normalize
2)什么时候用 method
| 判断项 | 用 method |
|---|---|
| 逻辑依附某个类型 | 是 |
| 需要读该类型字段 | 是 |
| 需要修改该类型字段 | 是 |
| 想表达领域行为 | 是 |
| 需要实现接口 | 是 |
典型例子:
type Cart struct {
Items []Item
}
func (c *Cart) Add(item Item) {
c.Items = append(c.Items, item)
}
func (c Cart) TotalAmount() int64 {
var total int64
for _, item := range c.Items {
total += item.Price
}
return total
}
method 适合的关键词
- Add
- Remove
- Update
- Close
- Reset
- Execute
- TotalAmount
- IsExpired
- CanRetry
3)什么时候用 interface
| 判断项 | 用 interface |
|---|---|
| 调用方不想依赖具体实现 | 是 |
| 确实有替换需求 | 是 |
| 需要 mock/fake | 是 |
| 是标准库约定行为 | 是 |
| 只是“可能将来会有多个实现” | 否,先别用 |
好的接口特点:
- 小
- 稳定
- 面向使用场景
- 不暴露多余能力
好例子:
type Clock interface {
Now() time.Time
}
坏例子:
type UserService interface {
Create()
Update()
Delete()
Get()
List()
BatchCreate()
BatchUpdate()
}
这类“大而全接口”通常不 Go。
4)什么时候用 NewXxx
| 判断项 | 用 NewXxx |
|---|---|
| 初始化必须传依赖 | 是 |
| 零值不可用 | 是 |
| 需要做参数校验 | 是 |
| 需要设置默认值 | 是 |
| 需要隐藏结构体字段 | 是 |
| 只是创建空对象 | 否 |
例如:
type Client struct {
baseURL string
httpCli *http.Client
}
func NewClient(baseURL string, httpCli *http.Client) *Client {
if httpCli == nil {
httpCli = http.DefaultClient
}
return &Client{
baseURL: baseURL,
httpCli: httpCli,
}
}
五、接收者怎么选:value 还是 pointer
这也是你做 method 设计时最常纠结的点。
value receiver
适合:
- 不修改状态
- struct 很小
- 语义上是值对象
- 希望避免共享修改
type Money struct {
Amount int64
Unit string
}
func (m Money) String() string {
return fmt.Sprintf("%d %s", m.Amount, m.Unit)
}
pointer receiver
适合:
- 需要修改状态
- struct 较大
- 含有 mutex、slice、map、复杂字段
- 希望方法集保持一致
type Buffer struct {
data []byte
}
func (b *Buffer) Write(p []byte) {
b.data = append(b.data, p...)
}
实战建议
只要一个类型有一些方法必须用指针接收者,那这个类型的大多数方法也尽量统一用指针接收者。
这样一致性更好,认知负担更低。
六、最常见的误区决策表
| 写法 | 是否推荐 | 原因 |
|---|---|---|
type XxxUtil struct{} + 一堆无状态方法 |
不推荐 | 纯函数更自然 |
所有东西都 New() 一下 |
不推荐 | 很多零值可用 |
| 所有模块先定义 interface 再实现 | 不推荐 | 容易过度抽象 |
| DTO / VO / Entity 全部塞满方法 | 视情况 | 不是所有数据都必须“对象化” |
| repo/service/client 使用 struct 聚合依赖 | 推荐 | 符合工程组织 |
| 领域对象包含必要行为 | 推荐 | 可维护约束和语义 |
| 小接口由消费方定义 | 推荐 | 最符合 Go 风格 |
七、你可以直接套用的模板
模板 1:纯函数型模块
package password
func Hash(raw string) (string, error) {
// ...
}
func Verify(raw, encoded string) bool {
// ...
}
适合:工具逻辑、算法逻辑、格式转换。
模板 2:领域对象方法
type Order struct {
Status string
}
func (o *Order) Pay() error {
if o.Status != "pending" {
return errors.New("invalid status")
}
o.Status = "paid"
return nil
}
适合:领域状态流转。
模板 3:应用服务
type OrderRepo interface {
Save(ctx context.Context, order Order) error
}
type OrderService struct {
repo OrderRepo
}
func NewOrderService(repo OrderRepo) *OrderService {
return &OrderService{repo: repo}
}
func (s *OrderService) Create(ctx context.Context, order Order) error {
return s.repo.Save(ctx, order)
}
适合:业务流程编排。
模板 4:标准库接口实现
type AppHandler struct{}
func (h *AppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
适合:和 Go 生态对接。
八、一个非常实用的速查版
你以后写代码时,可以直接按这个表秒判。
| 问题 | 是 | 否 |
|---|---|---|
| 这段逻辑有状态吗? | method / struct | 看下一条 |
| 这段逻辑依赖外部资源吗? | struct + method | 看下一条 |
| 这段逻辑天然属于某个领域对象吗? | method | 看下一条 |
| 只是计算/转换/校验吗? | function | 看下一条 |
| 需要替换实现吗? | interface | 先别抽象 |
| 零值不可用吗? | NewXxx |
直接初始化 |
九、我给你的团队级建议
如果你自己写项目,或者做 code review,可以定一个很实用的规范:
推荐规范
- 默认先写 function
- 出现共享依赖时,再提炼 service struct
- 出现明确领域状态时,再提炼 domain method
- interface 只放在消费者侧
NewXxx只为“可用初始化”服务- 禁止空壳
Util/Manager/Helperstruct,除非能说明状态或依赖是什么
这套规范很适合 Go 团队落地。
十、最后给你一句最落地的判断口诀
你可以记这个:
无状态,用函数;有状态,用方法;有依赖,用结构体;要替换,用小接口;要保证初始化,才写 New。
这句话基本能覆盖 80% 以上的 Go 设计决策。
如果你愿意,我下一条可以继续给你补一份:“结合真实业务目录结构的 Go 分层示例(controller/service/repo/domain/util)” ,把这张决策表落到项目代码组织上。
