web开发一
1. 代码分析
我们先来分析一下自动生成的代码,了解go-zero开发的基本逻辑
//服务上下文 依赖注入,需要用到的依赖都在此进行注入,比如配置,数据库连接,redis连接等
ctx := svc.NewServiceContext(c)
//注册路由
handler.RegisterHandlers(server, ctx)
1
2
3
4
2
3
4
handler:
func Hello01Handler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
//请求参数
var req types.Request
//解析参数
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
//注入serviceContext
l := logic.NewHello01Logic(r.Context(), svcCtx)
//业务逻辑实现
resp, err := l.Hello01(&req)
//返回响应结果
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
//返回json数据
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
所以用go-zero开发,遵循以下步骤:
- 编写
api
文件 - 生成代码
- 编写logic代码
2. 注册登录
接下来,我们以一个注册登录接口的例子,来讲解使用go-zero开发web应用
2.1 编写api文件
user.api
syntax = "v1"
type RegisterReq {
//代表可以接收json参数 并且是必填参数 注意 go-zero不支持多tag
Username string `json:"username"`
Password string `json:"password"`
}
type RegisterResp {}
type LoginReq {
Username string `json:"username"`
Password string `json:"password"`
}
type LoginResp {
Token string `json:"token"`
}
@server (
//代表当前service的代码会放在account目录下
//这里注意 冒汗要紧贴着key
group: account
//路由前缀
prefix: v1
)
//影响配置文件名称和主文件名称
service user-api {
//handler中的函数名称
@handler register
post /user/register (RegisterReq) returns (RegisterResp)
@handler login
post /user/login (LoginReq) returns (LoginResp)
}
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
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
//生成代码
# goctl api go --api user.api --dir .
1
2
2
2.2 添加数据库支持
sql:
CREATE TABLE `user` (
`id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`register_time` datetime NOT NULL,
`last_login_time` datetime NOT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
配置:
Name: user-api
Host: 0.0.0.0
Port: 8888
mysqlConfig:
datasource: "root:root@tcp(127.0.0.1:3306)/zero_test?charset=utf8mb4&parseTime=True&loc=Local"
connectTimeout: 10
1
2
3
4
5
6
2
3
4
5
6
type Config struct {
rest.RestConf
MysqlConfig MysqlConfig
}
type MysqlConfig struct {
DataSource string
ConnectTimeout int64
}
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
初始化数据库连接:
package db
import (
"context"
"github.com/zeromicro/go-zero/core/stores/sqlx"
"time"
"user-api/internal/config"
)
func NewMysql(mysqlConfig config.MysqlConfig) sqlx.SqlConn {
mysql := sqlx.NewMysql(mysqlConfig.DataSource)
db, err := mysql.RawDB()
if err != nil {
panic(err)
}
cxt, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(mysqlConfig.ConnectTimeout))
defer cancel()
err = db.PingContext(cxt)
if err != nil {
panic(err)
}
db.SetMaxOpenConns(100)
db.SetMaxIdleConns(10)
return mysql
}
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
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
注入serviceContext:
type ServiceContext struct {
Config config.Config
Conn sqlx.SqlConn
}
func NewServiceContext(c config.Config) *ServiceContext {
sqlConn := db.NewMysql(c.MysqlConfig)
return &ServiceContext{
Config: c,
Conn: sqlConn,
}
}
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
使用goctl model
命令生成代码
# go get github.com/go-sql-driver/mysql
# goctl model mysql ddl --src user.sql --dir .
1
2
2
2.3 编写代码逻辑
func (l *RegisterLogic) Register(req *types.RegisterReq) (resp *types.RegisterResp, err error) {
userModel := user.NewUserModel(l.svcCtx.Conn)
u, err := userModel.FindByUsername(l.ctx, req.Username)
if err != nil {
l.Logger.Error("Register FindByUsername err: ", err)
return nil, err
}
if u != nil {
//代表已经注册
return nil, errors.New("此用户名已经注册")
}
_, err = userModel.Insert(l.ctx, &user.User{
Username: req.Username,
Password: req.Password,
RegisterTime: time.Now(),
LastLoginTime: time.Now(),
})
if err != nil {
return nil, err
}
return
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func (m *customUserModel) FindByUsername(ctx context.Context, username string) (*User, error) {
query := fmt.Sprintf("select %s from %s where `username` = ? limit 1", userRows, m.table)
var resp User
err := m.conn.QueryRowCtx(ctx, &resp, query, username)
switch err {
case nil:
return &resp, nil
case sql.ErrNoRows, sqlx.ErrNotFound:
return nil, nil
default:
return nil, err
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
2.4 处理响应
上述代码我们在测试时,发现成功的时候返回null,有错误时返回错误信息,状态为400,在实际开发的过程中,我们期望返回格式化的响应信息,比如json,类似下面这样:
{
code: 200,
msg: "success",
data: null
}
1
2
3
4
5
2
3
4
5
{
code: 10100,
msg: "fail",
data: null
}
1
2
3
4
5
2
3
4
5
根据返回的code进行业务逻辑的处理,注意此时http的状态为200
如果http的状态为:
- 400,参数格式有问题
- 401,未认证
- 403,无权限
- 404,找不到接口
- 500,服务器错误
这些错误,可以进行统一的处理,我们需要将其和业务逻辑错误分开处理
2.4.1 自定义error
package biz
type Error struct {
Code int `json:"code"`
Msg string `json:"msg"`
}
func NewError(code int, msg string) *Error {
return &Error{
Code: code,
Msg: msg,
}
}
func (e *Error) Error() string {
return e.Msg
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package biz
const Ok = 200
var (
DBError = NewError(10000, "数据库错误")
AlreadyRegister = NewError(10100, "用户已注册")
)
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
2.4.2 统一返回
package biz
type Result struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data any `json:"data"`
}
func Success(data any) *Result {
return &Result{
Code: Ok,
Msg: "success",
Data: data,
}
}
func Fail(err *Error) *Result {
return &Result{
Code: err.Code,
Msg: err.Msg,
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2.4.3 统一错误处理
// httpx.SetErrorHandler 仅在调用了 httpx.Error 处理响应时才有效。
httpx.SetErrorHandler(func(err error) (int, any) {
switch e := err.(type) {
case *biz.Error:
return http.StatusOK, biz.Fail(e)
default:
return http.StatusInternalServerError, nil
}
})
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
func (l *RegisterLogic) Register(req *types.RegisterReq) (resp *types.RegisterResp, err error) {
userModel := user.NewUserModel(l.svcCtx.Conn)
u, err := userModel.FindByUsername(l.ctx, req.Username)
if err != nil {
l.Logger.Error("Register FindByUsername err: ", err)
return nil, biz.DBError
}
if u != nil {
//代表已经注册
return nil, biz.AlreadyRegister
}
_, err = userModel.Insert(l.ctx, &user.User{
Username: req.Username,
Password: req.Password,
RegisterTime: time.Now(),
LastLoginTime: time.Now(),
})
if err != nil {
return nil, err
}
return
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
httpx.OkJsonCtx(r.Context(), w, biz.Success(resp))
1
2.4.4 zero扩展包
关于
code和msg
这种形式,go-zero在https://github.com/zeromicro/x
扩展包中做了支持
导入扩展包依赖
go get github.com/zeromicro/x
1看一下扩展包中,我们需要用到的结构
package errors import "fmt" // CodeMsg is a struct that contains a code and a message. // It implements the error interface. type CodeMsg struct { Code int Msg string } func (c *CodeMsg) Error() string { return fmt.Sprintf("code: %d, msg: %s", c.Code, c.Msg) } // New creates a new CodeMsg. func New(code int, msg string) error { return &CodeMsg{Code: code, Msg: msg} }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// BaseResponse is the base response struct. type BaseResponse[T any] struct { // Code represents the business code, not the http status code. Code int `json:"code" xml:"code"` // Msg represents the business message, if Code = BusinessCodeOK, // and Msg is empty, then the Msg will be set to BusinessMsgOk. Msg string `json:"msg" xml:"msg"` // Data represents the business data. Data T `json:"data,omitempty" xml:"data,omitempty"` }
1
2
3
4
5
6
7
8
9
10将我们自定义的内容,换成扩展包的实现
package biz import "github.com/zeromicro/x/errors" const Ok = 200 //var ( // DBError = NewError(10000, "数据库错误") // AlreadyRegister = NewError(10100, "用户已注册") //) var ( DBError = errors.New(10000, "数据库错误") AlreadyRegister = errors.New(10100, "用户已注册") )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import ( "github.com/zeromicro/go-zero/rest/httpx" xhttp "github.com/zeromicro/x/http" "net/http" "user-api/internal/logic/account" "user-api/internal/svc" "user-api/internal/types" ) func RegisterHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req types.RegisterReq if err := httpx.Parse(r, &req); err != nil { xhttp.JsonBaseResponseCtx(r.Context(), w, err) return } l := account.NewRegisterLogic(r.Context(), svcCtx) resp, err := l.Register(&req) if err != nil { xhttp.JsonBaseResponseCtx(r.Context(), w, err) } else { xhttp.JsonBaseResponseCtx(r.Context(), w, resp) } } }
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去掉
httpx.SetErrorHandler
,然后测试即可
问题1:这样返回的成功code,默认为0,如果想变更需要修改源码来实现
问题2:handler的代码需要修改模板,要不然每次都需要重新变更
- 建议使用自定义方式
- 如果使用扩展包,建议fork代码后使用,维护方便
2.5 登录实现
登录逻辑为:
- 校验用户名密码
- 生成token
token生成:
// @secretKey: JWT 加解密密钥
// @iat: 时间戳
// @seconds: 过期时间,单位秒
// @payload: 数据载体
func GetJwtToken(secretKey string, iat, seconds int64, payload any) (string, error) {
claims := make(jwt.MapClaims)
claims["exp"] = iat + seconds
claims["iat"] = iat
claims["userId"] = payload
token := jwt.New(jwt.SigningMethodHS256)
token.Claims = claims
return token.SignedString([]byte(secretKey))
}
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
jwt相关配置:
Auth:
# 必须是8位以上
secret: "secret123456"
expire: 3600
1
2
3
4
2
3
4
type Config struct {
rest.RestConf
MysqlConfig MysqlConfig
Auth Auth
}
type MysqlConfig struct {
DataSource string
ConnectTimeout int64
}
type Auth struct {
Secret string
Expire int64
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
登录逻辑代码:
func (l *LoginLogic) Login(req *types.LoginReq) (resp *types.LoginResp, err error) {
// todo: add your logic here and delete this line
userModel := user.NewUserModel(l.svcCtx.Conn)
u, err := userModel.FindByUsernameAndPwd(l.ctx, req.Username, req.Password)
if err != nil {
l.Logger.Error(err)
return nil, biz.DBError
}
if u == nil {
return nil, biz.NameOrPwdError
}
//登录成功 生成token
secret := l.svcCtx.Config.Auth.Secret
expire := l.svcCtx.Config.Auth.Expire
token, err := biz.GetJwtToken(secret, time.Now().Unix(), expire, u.Id)
if err != nil {
l.Logger.Error(err)
return nil, biz.TokenError
}
resp = &types.LoginResp{
Token: token,
}
return
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2.6 获取用户信息
登录成功后,前端获取到token,通过token请求用户信息,go-zero框架已经支持jwt,只需要添加少量代码,就可以支持jwt认证
server.AddRoutes(
[]rest.Route{
{
Method: http.MethodGet,
Path: "/user/info",
Handler: account.GetUserInfoHandler(serverCtx),
},
},
rest.WithPrefix("/v1"),
//开启jwt认证
rest.WithJwt(serverCtx.Config.Auth.Secret),
)
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
syntax = "v1"
type RegisterReq {
//代表可以接收json参数 并且是必填参数 注意 go-zero不支持多tag
Username string `json:"username"`
Password string `json:"password"`
}
type RegisterResp {}
type LoginReq {
Username string `json:"username"`
Password string `json:"password"`
}
type LoginResp {
Token string `json:"token"`
}
type UserInfoResp {
Id int64 `json:"id"`
Username string `json:"username"`
}
@server (
//代表当前service的代码会放在account目录下
//这里注意 冒汗要紧贴着key
group: account
//路由前缀
prefix: v1
)
//影响配置文件名称和主文件名称
service user-api {
//handler中的函数名称
@handler register
post /user/register (RegisterReq) returns (RegisterResp)
@handler login
post /user/login (LoginReq) returns (LoginResp)
@handler getUserInfo
get /user/info returns (UserInfoResp)
}
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
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
逻辑代码:
func (l *GetUserInfoLogic) GetUserInfo() (resp *types.UserInfoResp, err error) {
//如果认证通过 可以从ctx中获取jwt payload
userId, err := l.ctx.Value("userId").(json.Number).Int64()
if err != nil {
return nil, biz.InvalidToken
}
u, err := user.NewUserModel(l.svcCtx.Conn).FindOne(l.ctx, userId)
if err != nil && (errors.Is(err, user.ErrNotFound) ||
errors.Is(err, sql.ErrNoRows)) {
return nil, biz.UserNotExist
}
resp = &types.UserInfoResp{
Id: userId,
Username: u.Username,
}
return
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
在HTTP请求添加名为Authorization
的header,形式如下
//这里注意 Bearer后面只有一个空格
Authorization: Bearer <token>
1
2
2
至此 我们完成了基本的web开发,具备了使用go-zero开发web应用的能力