Go 标准分层结构与扁平化 pkg 模块(个人偏好的实践分享)
在 Go(Golang)项目中,如何组织目录结构是一个高频问题。
Go 没有强制规范,但社区里逐渐沉淀出了一些常见做法。
这篇文章介绍我个人更倾向的一种改良:扁平化的 pkg 模块结构。
扁平化的 pkg 模块结构
虽然标准分层很常见,但在实际开发中会遇到一个问题:
子目录太多,容易出现同名 package,import 时让人混淆。
于是我更偏好一种改良方案:在 pkg/<模块名>/ 下,直接放置 service.go、repository.go、models.go、errors.go。这样每个模块只有一个 package,目录更干净。
示例结构
myapp/
├── cmd/
│ └── myapp/
│ └── main.go
├── internal/
│ └── config/
├── pkg/
│ ├── user/
│ │ ├── models.go
│ │ ├── errors.go
│ │ ├── repository.go
│ │ └── service.go
│ └── order/
│ ├── models.go
│ ├── errors.go
│ ├── repository.go
│ └── service.go
├── go.mod
└── README.md
模块示例:user
models.go
package user
type User struct {
ID string
Name string
}
errors.go
package user
import "errors"
var (
ErrNotFound = errors.New("user not found")
ErrAlreadyExists = errors.New("user already exists")
ErrInvalidInput = errors.New("invalid input")
)
repository.go
package user
import "sync"
type Repository interface {
GetByID(id string) (User, error)
Create(u User) error
Update(u User) error
Delete(id string) error
}
type InMemoryRepo struct {
mu sync.RWMutex
m map[string]User
}
func NewInMemoryRepo() *InMemoryRepo {
return &InMemoryRepo{m: make(map[string]User)}
}
func (r *InMemoryRepo) GetByID(id string) (User, error) {
r.mu.RLock()
defer r.mu.RUnlock()
u, ok := r.m[id]
if !ok {
return User{}, ErrNotFound
}
return u, nil
}
// 省略 Create/Update/Delete 的实现,逻辑类似
service.go
package user
import "strings"
type Service interface {
GetByID(id string) (User, error)
Create(u User) error
Rename(id, newName string) error
}
type DefaultService struct {
repo Repository
}
func NewDefaultService(r Repository) *DefaultService {
return &DefaultService{repo: r}
}
func (s *DefaultService) GetByID(id string) (User, error) {
return s.repo.GetByID(id)
}
func (s *DefaultService) Create(u User) error {
if strings.TrimSpace(u.ID) == "" || strings.TrimSpace(u.Name) == "" {
return ErrInvalidInput
}
return s.repo.Create(u)
}
func (s *DefaultService) Rename(id, newName string) error {
if strings.TrimSpace(newName) == "" {
return ErrInvalidInput
}
u, err := s.repo.GetByID(id)
if err != nil {
return err
}
u.Name = newName
return s.repo.Update(u)
}
三、应用入口(main.go)
package main
import (
"fmt"
"example.com/myapp/pkg/user"
)
func main() {
repo := user.NewInMemoryRepo()
svc := user.NewDefaultService(repo)
u := user.User{ID: "u_001", Name: "Alice"}
if err := svc.Create(u); err != nil {
panic(err)
}
got, err := svc.GetByID("u_001")
if err != nil {
panic(err)
}
fmt.Printf("hello, %s\n", got.Name)
}
运行结果:
hello, Alice
对比与总结
标准分层结构
- 优点:层次分明、适合大型项目、社区普遍接受。
- 缺点:子目录较多,容易出现 package 名冲突。
扁平化 pkg 模块
- 优点:每个模块只有一个 package,import 更直观;结构简洁。
- 缺点:当模块特别复杂时,可能需要更细的子目录拆分。
我的选择
我更倾向在中小规模项目里,使用 扁平化 pkg 模块结构:
- 每个模块独立目录,内部职责用文件区分;
- 避免同名 package 的困扰;
- 保留分层思想,同时保证简洁。
当项目进一步扩大时,再考虑是否演进为 独立模块(module) 或 独立仓库。