GO-ZERO
01基础001快速入门
01基础002配置文件
01基础003web开发一
01基础004web开发二
本文档使用 MrDoc 发布
-
+
首页
01基础003web开发一
## 1. 代码分析 > 我们先来分析一下自动生成的代码,了解go-zero开发的基本逻辑 ```go //服务上下文 依赖注入,需要用到的依赖都在此进行注入,比如配置,数据库连接,redis连接等 ctx := svc.NewServiceContext(c) //注册路由 handler.RegisterHandlers(server, ctx) ``` handler: ```go 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) } } } ``` 所以用go-zero开发,遵循以下步骤: - 1. 编写api文件 - 2. 生成代码 - 3. 编写logic代码 ## 2. 注册登录 > 接下来,我们以一个注册登录接口的例子,来讲解使用go-zero开发web应用 ### 2.1 编写api文件 user.api ```go 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) } ``` //生成代码 # `goctl api go --api user.api --dir .` ###2.2 添加数据库支持 sql: ```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; ``` 配置: ```yaml 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 ``` ```go type Config struct { rest.RestConf MysqlConfig MysqlConfig } type MysqlConfig struct { DataSource string ConnectTimeout int64 } ``` 初始化数据库连接: ```go 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 } ``` 注入serviceContext: ```go 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, } } ``` 使用goctl model命令生成代码 # `go get github.com/go-sql-driver/mysql` # `goctl model mysql ddl --src user.sql --dir .` ### 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 } 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 } } ``` ### 2.4 处理响应 > 上述代码我们在测试时,发现成功的时候返回null,有错误时返回错误信息,状态为400,在实际开发的过程中,我们期望返回格式化的响应信息,比如json,类似下面这样: ```json { code: 200, msg: "success", data: null } { code: 10100, msg: "fail", data: null } ``` 根据返回的code进行业务逻辑的处理,注意此时http的状态为200 如果http的状态为: 400,参数格式有问题 401,未认证 403,无权限 404,找不到接口 500,服务器错误 这些错误,可以进行统一的处理,我们需要将其和业务逻辑错误分开处理 #2.4.1 自定义error ```go 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 } package biz const Ok = 200 var ( DBError = NewError(10000, "数据库错误") AlreadyRegister = NewError(10100, "用户已注册") ) ``` #2.4.2 统一返回 ```go 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, } } ``` #2.4.3 统一错误处理 ```go // 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 } }) 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 } ``` ``` httpx.OkJsonCtx(r.Context(), w, biz.Success(resp)) ``` #### 2.4.4 zero扩展包 > 关于code和msg这种形式,go-zero在https://github.com/zeromicro/x扩展包中做了支持 导入扩展包依赖 `go get github.com/zeromicro/x` 看一下扩展包中,我们需要用到的结构 ```go 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} } // 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"` } ``` 将我们自定义的内容,换成扩展包的实现 ```go 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, "用户已注册") ) 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) } } } ``` 去掉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)) } jwt相关配置: Auth: # 必须是8位以上 secret: "secret123456" expire: 3600 type Config struct { rest.RestConf MysqlConfig MysqlConfig Auth Auth } type MysqlConfig struct { DataSource string ConnectTimeout int64 } type Auth struct { Secret string Expire int64 } 登录逻辑代码: 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 } ``` ### 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), ) 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) } 逻辑代码: 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 } 在HTTP请求添加名为Authorization的header,形式如下 //这里注意 Bearer后面只有一个空格 Authorization: Bearer <token> 至此 我们完成了基本的web开发,具备了使用go-zero开发web应用的能力
admin
2025年1月9日 10:00
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
Markdown文件
分享
链接
类型
密码
更新密码