今天开始学习Gin的项目分层,对于Java程序员来说,这很轻松,但是Go与Java多少有些差异。不过大体上都是通用的。

我将goweb项目的目录做了下归类。将main()里面的路由提取到专门管理路由的包里面。还有业务逻辑、错误处理、DTO等等。目前的项目结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
goweb/
├── go.mod
├── main.go
├── errs/ // 业务错误
│ └── error.go
├── middleware/ // 中间件
│ └── handlers.go
├── model/
│ └── user_dto.go // 存放所有用户相关的输入输出结构体
├── handler/ // 处理HTTP参数和响应 相当于controller,handler是Go风格,controller是java风格,用哪个都行
│ └── user_handler.go
├── services/ // 处理业务逻辑
└── user_service.go
└── respository/ // 处理数据访问
└── user_resppsitory.go

对于分层,必要性和Java一样,避免所有的逻辑都写在同一个文件里,少了复用性和整洁。不管什么语言,一般也都会分成handler -> service -> respository

model层

也就是存放一些DTO的目录。model/user_dto.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package model

import "time"

type User struct {
ID int
Name string
Email string
Age int
Password string
Status string
CreateAt time.Time
UpdateAt time.Time
}

type CreateUserReq struct {
ID int `json:"id"`
Name string `json:"name" binding:"required,min=2,max=5"`
Age int `json:"age" binding:"required,gte=18,lte=120"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6,max=20"`
Address string `json:"address"`
}

type UserResponse struct {
ID int `json:"id"`
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"email"`
Password string `json:"password,omitempty"`
Role string `json:"role" binding:"required,oneof=admin user"`
}

const (
UserStatusActive = "active"
UserStatusDisable = "disabled"
)

ModelRequestDTOResponseDTO建议分开:Model表示内部数据结构,RequestDTO表示入参,ResponseDTO表示出参。这样避免将敏感字段返回给前端,也能避免数据库结构变化影响API。

reponsitory层

这一层还没有学习,实际上就是数据访问层,对应Spring的dao/mapper。先模拟一下:

repository/user_repository.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package respository

import (
"sync"

"dev.net.cn/goweb/model"
)

type UserRepository struct {
mu sync.RWMutex
nextID int
users map[int]*model.User
}

func NewUserRepository() *UserRepository {
return &UserRepository{
nextID: 1,
users: make(map[int]*model.User),
}
}

func (r *UserRepository) Create(user *model.User) (*model.User, error) {
r.mu.Lock()
defer r.mu.Unlock()

user.ID = r.nextID
r.nextID++

r.users[user.ID] = user
return user, nil
}

service层

处理业务逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package service

import (
"context"
"time"

"dev.net.cn/goweb/model"
"dev.net.cn/goweb/repository"
)

type UserService struct {
userRepo *repository.UserRepository
}

func NewUserService(userRepo *repository.UserRepository) *UserService {
return &UserService{
userRepo: userRepo,
}
}

type CreateUserInput struct {
Name string
Email string
Password string
}

func (s *UserService) CreateUser(ctx context.Context, input CreateUserInput) (*model.User, error) {
user := &model.User{
Name: input.Name,
Email: input.Email,
Password: input.Password,
Status: model.UserStatusActive,
CreateAt: time.Now(),
UpdateAt: time.Now(),
}
//调用repository
return s.userRepo.Create(user)
}

service方法传递context.Context的原因是context用于传递请求生命周期超时取消信号。数据库查询、RPC调用、外部API调用都应该支持context,这样请求取消时下游操作也能及时停止。

handler层

handler/user_handler.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
package handler

import (
"strconv"
"strings"

"dev.net.cn/goweb/errs"
"dev.net.cn/goweb/model"
"dev.net.cn/goweb/response"
"dev.net.cn/goweb/service"
"github.com/gin-gonic/gin"
)

type UserHandler struct {
userService *service.UserService
}

func NewUserHandler(userService *service.UserService) *UserHandler {
return &UserHandler{
userService: userService,
}
}

func GetUserListHandler(c *gin.Context) {
page, err := strconv.Atoi(c.DefaultQuery("page", "1"))
if err != nil {
response.BadRequest(c, err.Error())
return
}
pageSize, err := strconv.Atoi(c.DefaultQuery("page_size", "10"))
if err != nil {
response.BadRequest(c, err.Error())
return
}

response.PageOK(c, []gin.H{
{"name": "Tom", "age": 18},
{"name": "Jerry", "age": 16},
{"name": "Tony", "age": 28},
}, page, pageSize)
}

func GetUserInfoByIdHandler(c *gin.Context) {
id := c.Param("id")
if id == "" {
response.BadRequest(c, "id is null")
return
}

response.OK(c, gin.H{
"id": id,
"name": "tom",
"age": 18,
})
}

func (h *UserHandler) CreateUserHandler(c *gin.Context) {
var user model.CreateUserReq
if err := c.ShouldBindJSON(&user); err != nil {
response.BadRequest(c, err.Error())
return
}
if err := validateEmail(user.Email); err != nil {
response.HandleError(c, err)
return
}

user1, err := h.userService.CreateUser(c.Request.Context(), service.CreateUserInput{
Name: user.Name,
Age: user.Age,
Email: user.Email,
Password: user.Password,
})

if err != nil {
response.BadRequest(c, err.Error())
return
}

res := model.UserResponse{
ID: 1,
Name: user1.Name,
Age: user1.Age,
Email: user1.Email,
}
response.OK(c, res)
}

func UpdateUserHandler(c *gin.Context) {
id := c.Param("id")
if id == "" {
response.BadRequest(c, "id is null")
return
}
// 模拟根据ID获取用户

var user model.CreateUserReq

if err := c.ShouldBindJSON(&user); err != nil {
response.BadRequest(c, err.Error())
}

res := model.UserResponse{
ID: 1,
Name: user.Name,
Age: user.Age,
Email: user.Email,
}
response.OK(c, res)

}

func DeleteUserByIdHandler(c *gin.Context) {
id := c.Param("id")
if id == "" {
response.BadRequest(c, "id is null")
return
}

response.OK(c, "用户已被删除")
}

// FormLogin /*
func FormLogin(c *gin.Context) {
username := c.PostForm("username")
password := c.PostForm("password")

response.OK(c, gin.H{
"username": username,
"password": password,
})
}

func validateEmail(email string) error {
if strings.HasSuffix(email, "@qq.com") {
return nil
}
return errs.ErrEmailExists
}

这里只做了一个模拟例子:func (h *UserHandler) CreateUserHandler(c *gin.Context)

此时就完成了handler -> service -> repository的模拟。

HandlerService的边界:Handler处理协议层的问题,例如JSON Body、路径参数、相应格式;Service处理业务规则,例如创建用户、校验角色、判断权限等。Service不应该依赖Gin

routers层

这里统一管理路由:routers/route.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package routers

import (
"dev.net.cn/goweb/handler"
"dev.net.cn/goweb/middleware"
"dev.net.cn/goweb/repository"
"dev.net.cn/goweb/service"
"github.com/gin-gonic/gin"
)

func SetupRouter() *gin.Engine {

r := gin.New()
r.Use(gin.Recovery())
//r.Use(LoggerMiddleware())
r.Use(gin.Logger())

// 创建
userRepo := repository.NewUserRepository()
userService := service.NewUserService(userRepo)
userHandler := handler.NewUserHandler(userService)

// RESTful API
d1Group := r.Group("/api/d1/")
{
d1Group.GET("/users", handler.GetUserListHandler)
d1Group.GET("/users/:id", handler.GetUserInfoByIdHandler)
d1Group.POST("/users", userHandler.CreateUserHandler)
d1Group.PUT("/users/:id", handler.UpdateUserHandler)
d1Group.DELETE("users/:id", handler.DeleteUserByIdHandler)
}

// 访问该链路需要token认证
d2Group := r.Group("/api/d2").Use(middleware.AuthMiddleware())
{
d2Group.GET("/auth", handler.GetUserListHandler)
}

return r
}

对于main.go,仅需要如下代码:

1
2
3
4
5
6
7
8
9
func main() {
// 创建一个新的Gin路由引擎(不带任何中间件)
r := routers.SetupRouter()

err := r.Run(":8080")
if err != nil {
fmt.Println("start error...")
}
}