本文为《Java工程师转Go实战》连载 第 18 篇 / 共 20 篇
上一篇:GMP 与 GC
下一篇:Java CRUD 迁移实战
类比开场
Java 经典分层:
Controller → Service → Repository/DAO → DB
↓ ↓ ↓
DTO Domain Entity/PO
Go 完全可以 原样迁移这套分层,不必追求奇技淫巧。Java 人最快的上手路径就是:把你熟悉的三层架构用 Go 语法重写一遍。
但 Go 的包机制和 Java 的 class 体系有本质差异------理解这些差异,才能写出 地道且不绕弯 的 Go 项目结构。
一、Go 项目布局标准
社区共识:golang-standards/project-layout
your-service/
├── cmd/ # 入口(可以有多个二进制)
│ └── server/
│ └── main.go # 组装依赖、启动服务
├── internal/ # 私有代码(Go 编译器禁止外部 import)
│ ├── handler/ # HTTP/gRPC Handler(= Controller)
│ ├── service/ # 业务逻辑
│ ├── repository/ # 数据访问(= DAO)
│ ├── model/ # 数据模型
│ │ ├── entity.go # DB 实体(= @Entity)
│ │ ├── request.go # 请求 DTO
│ │ └── response.go # 响应 DTO
│ └── middleware/ # HTTP/gRPC 中间件
├── pkg/ # 可被外部引用的公共包
│ ├── response/ # 统一响应
│ └── errcode/ # 错误码定义
├── api/ # API 定义(proto / OpenAPI)
├── configs/ # 配置文件
├── scripts/ # 脚本(build、deploy)
├── migrations/ # 数据库迁移文件
├── docs/ # 文档
├── Dockerfile
├── Makefile
├── go.mod
└── go.sum
和 Java 项目结构对照
| Java (Maven) | Go | 说明 |
|---|---|---|
src/main/java/com/xxx/controller/ |
internal/handler/ |
处理 HTTP 请求 |
src/main/java/com/xxx/service/ |
internal/service/ |
业务逻辑 |
src/main/java/com/xxx/dao/ |
internal/repository/ |
数据访问 |
src/main/java/com/xxx/entity/ |
internal/model/entity.go |
DB 实体 |
src/main/java/com/xxx/dto/ |
internal/model/request.go |
DTO |
src/main/java/com/xxx/config/ |
configs/ + internal/config/ |
配置 |
src/main/java/com/xxx/Application.java |
cmd/server/main.go |
启动入口 |
pom.xml |
go.mod |
依赖管理 |
二、internal 包的意义
Go 编译器 硬性规定 :internal/ 目录下的代码只能被其父目录及同级的代码 import。
your-service/
├── internal/
│ └── service/user.go # 只有 your-service 内的代码能 import
└── cmd/server/main.go # ✅ 可以 import internal/service
other-project/
└── main.go # ❌ 编译报错:use of internal package not allowed
对标 Java :类似 Java 9 Module System 的 exports,但更简单粗暴------放 internal 就是私有,放 pkg 就是公开。
实践建议 :业务代码全部放 internal,只有真正需要跨项目复用的工具包放 pkg。
三、依赖方向(核心原则)
┌─────────────────────────────────────────────┐
│ cmd/main.go │ 组装所有依赖
│ (Composition Root) │
└─────────────┬───────────────────────────────┘
│ import
▼
┌─────────────────────────────────────────────┐
│ handler (Controller) │ 绑定参数、调 service、写响应
└─────────────┬───────────────────────────────┘
│ import
▼
┌─────────────────────────────────────────────┐
│ service (Business Logic) │ 业务规则、编排、事务
└─────────────┬───────────────────────────────┘
│ 依赖接口(不是具体实现)
▼
┌─────────────────────────────────────────────┐
│ repository (Data Access) │ SQL、缓存、第三方 API
└─────────────────────────────────────────────┘
所有层都可以 import:
┌─────────────────────────────────────────────┐
│ model (数据结构) │
└─────────────────────────────────────────────┘
关键规则
- handler → service → repository:单向依赖,禁止反向
- repository 不能 import handler(循环依赖编译报错)
- service 层定义 Repository 接口,repository 层提供实现(依赖倒置)
- model 包被所有层共享(纯数据结构,无业务逻辑)
四、各层代码示例
Model 层
go
// internal/model/entity.go
package model
import "time"
// DB 实体(对标 @Entity)
type User struct {
ID int64 `gorm:"primaryKey" json:"id"`
Name string `gorm:"size:64;not null" json:"name"`
Email string `gorm:"size:128;uniqueIndex" json:"email"`
Password string `gorm:"size:128" json:"-"` // json:"-" 不序列化
Status int8 `gorm:"default:1" json:"status"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
go
// internal/model/request.go
package model
// 请求 DTO(对标 Java 的 XxxReq/XxxDTO)
type CreateUserReq struct {
Name string `json:"name" binding:"required,min=1,max=64"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6,max=32"`
}
type UpdateUserReq struct {
Name *string `json:"name" binding:"omitempty,min=1,max=64"` // 指针:区分"未传"和"空"
Email *string `json:"email" binding:"omitempty,email"`
}
type ListUserReq struct {
Keyword string `form:"keyword"`
Page int `form:"page" binding:"gte=1"`
PageSize int `form:"pageSize" binding:"gte=1,lte=100"`
}
go
// internal/model/response.go
package model
// 响应 DTO(避免直接返回 entity,防止泄漏字段)
type UserResp struct {
ID int64 `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
func UserToResp(u *User) *UserResp {
if u == nil {
return nil
}
return &UserResp{ID: u.ID, Name: u.Name, Email: u.Email}
}
Repository 层
go
// internal/service/interfaces.go
// ⚠️ 接口定义在消费方(service 包),不是 repository 包
package service
import (
"context"
"your-service/internal/model"
)
type UserRepository interface {
Create(ctx context.Context, u *model.User) error
GetByID(ctx context.Context, id int64) (*model.User, error)
GetByEmail(ctx context.Context, email string) (*model.User, error)
Update(ctx context.Context, u *model.User) error
List(ctx context.Context, keyword string, offset, limit int) ([]*model.User, int64, error)
}
go
// internal/repository/user_repo.go
package repository
import (
"context"
"your-service/internal/model"
"gorm.io/gorm"
)
type userRepo struct {
db *gorm.DB
}
func NewUserRepo(db *gorm.DB) *userRepo {
return &userRepo{db: db}
}
func (r *userRepo) Create(ctx context.Context, u *model.User) error {
return r.db.WithContext(ctx).Create(u).Error
}
func (r *userRepo) GetByID(ctx context.Context, id int64) (*model.User, error) {
var u model.User
err := r.db.WithContext(ctx).First(&u, id).Error
return &u, err
}
func (r *userRepo) GetByEmail(ctx context.Context, email string) (*model.User, error) {
var u model.User
err := r.db.WithContext(ctx).Where("email = ?", email).First(&u).Error
return &u, err
}
func (r *userRepo) Update(ctx context.Context, u *model.User) error {
return r.db.WithContext(ctx).Save(u).Error
}
func (r *userRepo) List(ctx context.Context, keyword string, offset, limit int) ([]*model.User, int64, error) {
var users []*model.User
var total int64
query := r.db.WithContext(ctx).Model(&model.User{})
if keyword != "" {
query = query.Where("name LIKE ? OR email LIKE ?", "%"+keyword+"%", "%"+keyword+"%")
}
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
if err := query.Offset(offset).Limit(limit).Order("id DESC").Find(&users).Error; err != nil {
return nil, 0, err
}
return users, total, nil
}
Service 层
go
// internal/service/user_service.go
package service
import (
"context"
"errors"
"fmt"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
"your-service/internal/model"
)
var (
ErrUserNotFound = errors.New("user not found")
ErrDuplicateEmail = errors.New("email already exists")
ErrInvalidName = errors.New("invalid name")
)
type UserService struct {
repo UserRepository // 依赖接口,不是具体实现
}
func NewUserService(repo UserRepository) *UserService {
return &UserService{repo: repo}
}
func (s *UserService) Create(ctx context.Context, req model.CreateUserReq) (*model.User, error) {
// 检查邮箱是否已存在
existing, err := s.repo.GetByEmail(ctx, req.Email)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("check email: %w", err)
}
if existing != nil && existing.ID > 0 {
return nil, ErrDuplicateEmail
}
// 加密密码
hashed, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
return nil, fmt.Errorf("hash password: %w", err)
}
user := &model.User{
Name: req.Name,
Email: req.Email,
Password: string(hashed),
Status: 1,
}
if err := s.repo.Create(ctx, user); err != nil {
return nil, fmt.Errorf("create user: %w", err)
}
return user, nil
}
func (s *UserService) GetByID(ctx context.Context, id int64) (*model.User, error) {
user, err := s.repo.GetByID(ctx, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrUserNotFound
}
return nil, fmt.Errorf("get user: %w", err)
}
return user, nil
}
func (s *UserService) List(ctx context.Context, req model.ListUserReq) ([]*model.User, int64, error) {
offset := (req.Page - 1) * req.PageSize
return s.repo.List(ctx, req.Keyword, offset, req.PageSize)
}
Handler 层
go
// internal/handler/user_handler.go
package handler
import (
"errors"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"your-service/internal/model"
"your-service/internal/service"
"your-service/pkg/response"
)
type UserHandler struct {
svc *service.UserService
}
func NewUserHandler(svc *service.UserService) *UserHandler {
return &UserHandler{svc: svc}
}
func (h *UserHandler) Create(c *gin.Context) {
var req model.CreateUserReq
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
user, err := h.svc.Create(c.Request.Context(), req)
if err != nil {
h.handleError(c, err)
return
}
response.Created(c, model.UserToResp(user))
}
func (h *UserHandler) Get(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.BadRequest(c, "invalid id")
return
}
user, err := h.svc.GetByID(c.Request.Context(), id)
if err != nil {
h.handleError(c, err)
return
}
response.OK(c, model.UserToResp(user))
}
func (h *UserHandler) List(c *gin.Context) {
var req model.ListUserReq
req.Page = 1
req.PageSize = 20
if err := c.ShouldBindQuery(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
users, total, err := h.svc.List(c.Request.Context(), req)
if err != nil {
h.handleError(c, err)
return
}
list := make([]*model.UserResp, 0, len(users))
for _, u := range users {
list = append(list, model.UserToResp(u))
}
response.OKPage(c, list, total, req.Page, req.PageSize)
}
func (h *UserHandler) handleError(c *gin.Context, err error) {
switch {
case errors.Is(err, service.ErrUserNotFound):
response.NotFound(c, "用户不存在")
case errors.Is(err, service.ErrDuplicateEmail):
response.BadRequest(c, "邮箱已被注册")
default:
response.ServerError(c)
}
}
五、main 组装(对标 Spring IoC 容器)
go
// cmd/server/main.go
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
"context"
"net/http"
"time"
"github.com/gin-gonic/gin"
"your-service/internal/config"
"your-service/internal/handler"
"your-service/internal/middleware"
"your-service/internal/repository"
"your-service/internal/service"
)
func main() {
// 加载配置
cfg := config.Load()
// 初始化基础设施
db := initDB(cfg.Database)
rdb := initRedis(cfg.Redis)
logger := initLogger(cfg.Log)
// ====== 依赖组装(= Spring 容器的工作)======
userRepo := repository.NewUserRepo(db)
userSvc := service.NewUserService(userRepo)
userHandler := handler.NewUserHandler(userSvc)
orderRepo := repository.NewOrderRepo(db)
orderSvc := service.NewOrderService(orderRepo, rdb)
orderHandler := handler.NewOrderHandler(orderSvc)
// ====== 路由注册 ======
r := gin.New()
r.Use(
middleware.Logger(logger),
middleware.Recovery(logger),
middleware.RequestID(),
middleware.CORS(),
)
api := r.Group("/api/v1")
{
// 公开路由
api.POST("/register", userHandler.Create)
// 需要认证的路由
auth := api.Group("", middleware.Auth(cfg.JWT.Secret))
auth.GET("/users/:id", userHandler.Get)
auth.GET("/users", userHandler.List)
auth.POST("/orders", orderHandler.Create)
}
// ====== 优雅启动与关闭 ======
srv := &http.Server{
Addr: fmt.Sprintf(":%d", cfg.Server.Port),
Handler: r,
ReadTimeout: cfg.Server.ReadTimeout,
WriteTimeout: cfg.Server.WriteTimeout,
}
go func() {
logger.Info("server starting", "port", cfg.Server.Port)
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
logger.Error("listen failed", "error", err)
os.Exit(1)
}
}()
// 等待退出信号
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
logger.Info("shutting down...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
srv.Shutdown(ctx)
logger.Info("server stopped")
}
大型项目:用 google/wire 自动生成依赖注入
go
// wire.go(定义依赖关系,wire 工具自动生成组装代码)
//go:build wireinject
package main
import (
"github.com/google/wire"
"your-service/internal/handler"
"your-service/internal/repository"
"your-service/internal/service"
)
func InitializeApp(cfg *config.Config) (*App, error) {
wire.Build(
initDB,
initRedis,
repository.NewUserRepo,
repository.NewOrderRepo,
service.NewUserService,
service.NewOrderService,
handler.NewUserHandler,
handler.NewOrderHandler,
NewApp,
)
return nil, nil
}
bash
wire ./cmd/server/
# 生成 wire_gen.go,包含完整的依赖组装代码
对标 Spring 的 @Autowired,但是 编译期 生成,没有运行时反射开销。
六、和 DDD 的关系
| Java DDD 概念 | Go 实现 | 说明 |
|---|---|---|
| Aggregate Root | 大 struct + 包私有字段 | 用包边界保护不变式 |
| Domain Service | service 包中的函数/struct | |
| Repository (接口) | service 包定义的 interface | 消费方定义 |
| Infrastructure | repository 包 + 外部 SDK | |
| Application Service | handler + service 编排 | |
| Value Object | 小 struct(不可变) | Go 没有 final |
| Domain Event | channel / 消息队列 | |
| Anti-Corruption Layer | adapter 包 | 隔离第三方 |
实际的 DDD 目录(大型项目)
internal/
├── domain/
│ ├── user/
│ │ ├── entity.go # 聚合根
│ │ ├── repository.go # 仓储接口
│ │ ├── service.go # 领域服务
│ │ └── event.go # 领域事件
│ └── order/
│ ├── entity.go
│ ├── repository.go
│ └── service.go
├── application/ # 应用层(用例编排)
│ ├── user_usecase.go
│ └── order_usecase.go
├── interfaces/ # 接口层(HTTP/gRPC/MQ)
│ ├── http/
│ │ ├── user_handler.go
│ │ └── order_handler.go
│ └── grpc/
├── infrastructure/ # 基础设施
│ ├── persistence/ # 仓储实现
│ │ ├── mysql_user_repo.go
│ │ └── redis_cache.go
│ └── external/ # 外部服务
│ └── payment_gateway.go
注意:大多数项目不需要 DDD。简单三层够用就别过度设计。
七、常见陷阱
1. 循环依赖(Go 直接编译报错)
go
// ❌ service 和 handler 互相 import → 编译失败
// package service imports package handler
// package handler imports package service
// ✅ 解决:
// 方法 1:接口定义在消费方
// 方法 2:公共类型提取到 model 包
// 方法 3:拆分更细的包
2. 包层级太深
go
// ❌ Java 习惯:com.company.project.module.submodule.XXX
// internal/domain/user/service/impl/user_service_impl.go → 过度
// ✅ Go 习惯:扁平
// internal/service/user_service.go → 够了
3. 一个包一个文件 vs 一个包多个文件
go
// Go 的包不是"一个文件一个类"
// 一个包下可以有多个 .go 文件,它们共享包级变量和类型
// ✅ 合理的文件划分
internal/service/
├── user_service.go // UserService
├── order_service.go // OrderService
├── interfaces.go // 接口定义
└── errors.go // 包级错误定义
4. 不要 interface pollution
go
// ❌ 每个 struct 都定义一个 interface(Java 习惯)
type UserServiceInterface interface {
Create(...)
Get(...)
Update(...)
Delete(...)
}
// ✅ 只在需要 mock 或多态时才定义接口
// repository 需要接口(因为 service 测试要 mock)
// service 通常不需要接口(handler 直接依赖具体 struct)
八、Makefile:项目命令统一入口
makefile
.PHONY: build run test lint migrate
# 变量
APP_NAME := user-service
MAIN := ./cmd/server
build:
CGO_ENABLED=0 go build -o bin/$(APP_NAME) $(MAIN)
run:
go run $(MAIN)
test:
go test -race -short ./...
test-cover:
go test -race -coverprofile=coverage.out ./...
go tool cover -func=coverage.out | tail -1
lint:
golangci-lint run ./...
migrate-up:
migrate -database "$(DB_DSN)" -path migrations up
migrate-down:
migrate -database "$(DB_DSN)" -path migrations down 1
proto:
buf generate
wire:
wire ./cmd/server/
docker:
docker build -t $(APP_NAME):latest .
dev:
air
📦 面试追问
-
handler 能直接调 repo 吗?
小 demo 可以;生产应经 service,便于添加业务规则、事务和测试。handler 只做参数绑定+响应序列化。
-
DTO 和 Entity 要分吗?
建议分。
CreateUserReq(不含 ID、时间)/User(DB 字段完整)/UserResp(不含密码)。和 Java 一样防止字段泄漏和解耦变更。 -
go-zero / Kratos 结构不同?
go-zero 是 handler→logic→svc+model;Kratos 是 server→service→biz→data。本质仍是 handler-service-repo 三层,只是命名和代码生成约定不同。
-
什么时候用 wire?
依赖超过 10 个、多个入口(HTTP+gRPC+consumer)时手动组装太繁琐,wire 编译期生成保证类型安全。小项目手动
New()完全够用。 -
Go 怎么做 @Transactional?
没有注解。用
db.Transaction(func(tx *gorm.DB) error { ... })手动包装。如需跨 repository 事务,在 service 层传 tx 或用 unit of work 模式。
💡 一句话总结
Spring 那套三层别扔------换 Go 语法重写一遍,controller→handler,service→service,dao→repository,@Autowired→构造函数注入,加上 internal 限制可见性。比你强行上 DDD 框架更接地气,也是团队最快达成共识的方式。
建议标签 :Golang Java 项目结构 分层架构 Gin DDD wire