web开发一

1. 代码分析

我们先来分析一下自动生成的代码,了解go-zero开发的基本逻辑

//服务上下文 依赖注入,需要用到的依赖都在此进行注入,比如配置,数据库连接,redis连接等
ctx := svc.NewServiceContext(c)
//注册路由
handler.RegisterHandlers(server, ctx)
1
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

所以用go-zero开发,遵循以下步骤:

  1. 编写api文件
  2. 生成代码
  3. 编写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
//生成代码
# goctl api go --api user.api --dir .
1
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

配置:

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
type Config struct {
	rest.RestConf
	MysqlConfig MysqlConfig
}

type MysqlConfig struct {
	DataSource     string
	ConnectTimeout int64
}
1
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

注入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

使用goctl model命令生成代码

# go get github.com/go-sql-driver/mysql
# goctl model mysql ddl --src user.sql --dir .
1
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
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.4 处理响应

上述代码我们在测试时,发现成功的时候返回null,有错误时返回错误信息,状态为400,在实际开发的过程中,我们期望返回格式化的响应信息,比如json,类似下面这样:

{
    code: 200,
    msg: "success",
    data: null
}
1
2
3
4
5
{
    code: 10100,
    msg: "fail",
    data: null
}
1
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
package biz

const Ok = 200

var (
	DBError         = NewError(10000, "数据库错误")
	AlreadyRegister = NewError(10100, "用户已注册")
)

1
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.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
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
httpx.OkJsonCtx(r.Context(), w, biz.Success(resp))
1

2.4.4 zero扩展包

关于code和msg这种形式,go-zero在https://github.com/zeromicro/x扩展包中做了支持

  1. 导入扩展包依赖

    go get github.com/zeromicro/x
    
    1
  2. 看一下扩展包中,我们需要用到的结构

    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
  3. 将我们自定义的内容,换成扩展包的实现

    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
    16
    import (
    	"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
  4. 去掉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

jwt相关配置:

Auth:
  # 必须是8位以上
  secret: "secret123456"
  expire: 3600
1
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

登录逻辑代码:

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.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
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

逻辑代码:

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

在HTTP请求添加名为Authorization的header,形式如下

//这里注意 Bearer后面只有一个空格
Authorization: Bearer <token>
1
2

至此 我们完成了基本的web开发,具备了使用go-zero开发web应用的能力