GO-ZERO go-zero 是一个集成了各种工程实践的 web 和 rpc 框架。通过弹性设计保障了大并发服务端的稳定性,经受了充分的实战检验。
官网:https://go-zero.dev/docs/concepts/overview
环境安装 MySQL、Docker 略
goctl 安装 goctl 是 go-zero 的内置脚手架,是提升开发效率的一大利器,可以一键生成代码、文档、部署 k8s yaml、dockerfile 等。
下载
1 go install github.com/zeromicro/go-zero/tools/goctl@latest
验证版本
protoc 安装 protoc 是一个用于生成代码的工具,它可以根据 proto 文件生成C++、Java、Python、Go、PHP 等多重语言的代码,而 gRPC 的代码生成还依赖 protoc-gen-go ,protoc-gen-go-grpc 插件来配合生成 Go 语言的 gRPC 代码。
1 goctl env check --install --verbose --force
创建 go-zero 项目 HTTP 服务 推荐在 Goland 创建新项目(包括 go module 管理), 之后引入依赖
1 go get -u github.com/zeromicro/go-zero@latest
创建多个子目录作为多个微服务,在子目录用 goctl 生成名叫 demo 的 http服务
启动服务 在完成上述代码编写后,我们可以通过如下指令启动服务:
1 2 3 4 5 6 # 进入服务目录 $ cd ~/workspace/api/demo# 整理依赖文件 $ go mod tidy # 启动 go 程序 $ go run demo.go
gRPC 服务 ⬆️同理
etcd etcd 是基于 Raft 共识算法实现的、高可用的分布式键值(KV)存储系统 ,由 CoreOS 开发(现归属于 CNCF),主打强一致性、高可靠、轻量易用 ,是云原生生态的核心基础组件(比如 Kubernetes 全靠 etcd 实现集群状态的统一管理)。
简单来说:etcd 就像分布式系统的 “大脑” ,负责存储集群中所有**关键的配置信息、状态数据、元数据 **,并通过 Raft 保证这些数据在多节点间的强一致性,让分布式集群的所有节点对 “集群当前状态” 达成统一认知。
数据可靠性比Redis强
下载 通过Docker安装(单体架构) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 docker run -d \ --name etcd \ -p 2379:2379 \ -p 2380:2380 \ --restart always \ quay.io/coreos/etcd:v3.5.12 \ etcd \ --name s1 \ --listen-client-urls http://0.0.0.0:2379 \ --advertise-client-urls http://localhost:2379 \ --listen-peer-urls http://0.0.0.0:2380 \ --initial-advertise-peer-urls http://0.0.0.0:2380 \ --initial-cluster s1=http://0.0.0.0:2380 \ --initial-cluster-token tkn \ --initial-cluster-state new
下载客户端 etcdcli并存进环境变量 Etcd客户端下载:https://github.com/etcd-io/etcd/releases
Ps.环境配置踩坑 1. 网络身份认知错乱(Connection Refused / Restarting)
现象: 容器启动了但访问不到,或者无限重启。
原因:listen(监听地址)和advertise(宣告地址)没分清。
Listen: 容器内部 要监听 0.0.0.0,才能接收来自 Docker 网关的转发。如果只监听 localhost,外部无法访问。
Advertise: 告诉客户端(etcdctl)“怎么找我”。因为你在宿主机(Mac)上访问,必须告诉客户端找 localhost。
2. 版本废弃参数坑(Flag not defined)
现象: 报错 flag provided but not defined: -allow-none-authentication。
原因: 软件在迭代,教程在过时。 etcd 3.5+ 版本为了安全或规范,移除了一些旧参数。
经验: 以后用 Docker 跑中间件(MySQL/Redis/Kafka),如果照抄网上的命令报错,第一反应应该是“参数是不是改名或废弃了”,去 Docker Hub 或官网看最新文档。
3. 集群自检失败(Discovery Failed)
基本指令 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # 设置或更新值 etcdctl put name 张三 # 获取值 etcdctl get name # 只要value etcdctl get name --print-value-only # 获取name前缀的键值对 etcdctl get --prefix name # 删除键值对 etcdctl del name # 监听键的变化 etcdctl watch name
微服务Demo 这个案例分为user和video服务,video接受前端的http请求,内部方法通过grpc调用video服务的方法,user就是一个微服务的grpc接口。
具体为前端路径传参传入用户id调用Ping方法,最终返回用户的Id和name信息。name信息由负责grpc远程调用的user服务提供。
先生成gRPC接口 编辑proto文件 ,在user/rpc/rpc.proto
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 syntax = "proto3" ; package rpc ;option go_package="./rpc" ;service Rpc { rpc Ping(IdRequest) returns (UserResponse) ; } message IdRequest { string id = 1 ; } message UserResponse { string id = 1 ; string name = 2 ; string age = 3 ; }
生成代码指令
( cd到user上一级的项目根目录)
1 goctl rpc protoc user/rpc/rpc.proto --go_out=user/rpc/types --go-grpc_out=user/rpc/types --zrpc_out=user/rpc/
编辑逻辑
在user/rpc/intertal/logic/pinglogic.go
1 2 3 4 5 6 7 func (l *PingLogic) Ping(in *rpc.IdRequest) (*rpc.UserResponse, error) { return &rpc.UserResponse{ Id: in.Id, Name: "Jim", Age: "18", }, nil }
现在就可以使用gRPC远程调用这个方法了(在 apifox 测试)
编写 HTTP API 调用这个 gRPC 编辑 api 文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 type ( VideoReq { Id string `path:"id"` } VideoRes { Id string `json:"id"` Name string `json:"name"` } ) service video { @handler getVideo get /api/videos/:id (VideoReq) returns (VideoRes) }
生成指令代码
1 goctl api go -api video/api/video.api -dir video/api/
在 config/config.go 添加 Rpc 结构体
1 2 3 4 type Config struct { rest.RestConf UserRpc zrpc.RpcClientConf //添加 Rpc }
在 svc 绑定调用的微服务的结构体
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import ( "test_go_zero/user/rpc/rpcclient" "test_go_zero/video/api/internal/config" "github.com/zeromicro/go-zero/zrpc" ) type ServiceContext struct { Config config.Config UserRpc rpcclient.Rpc } func NewServiceContext (c config.Config) *ServiceContext { return &ServiceContext{ Config: c, UserRpc: rpcclient.NewRpc(zrpc.MustNewClient(c.UserRpc)), } }
编辑业务方法
1 2 3 4 5 6 7 8 9 10 func (l *GetVideoLogic) GetVideo(req *types.VideoReq) (resp *types.VideoRes, err error ) { ping, err := l.svcCtx.UserRpc.Ping(l.ctx, &rpc.IdRequest{Id: req.Id}) if err != nil { return nil , err } return &types.VideoRes{ Id: ping.Id, Name: ping.Name, }, nil }
最后,在 yaml 文件(etc/video.yml)绑定 etcd 端口和 key
1 2 3 4 5 6 7 8 9 10 11 Name: video Host: 0.0.0.0 Port: 8888 ---- UserRpc: Etcd: Hosts: - 127.0.0.1:2379 Key: rpc.rpc # 确保和远程调用的 rpc 的 key一致 ----
最后启动 http接口对应的主程序
API文件 统一包装响应 在common包下编写response.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 package commonimport ( "net/http" "github.com/zeromicro/go-zero/rest/httpx" ) type Body struct { Code uint32 `json:"code"` Msg string `json:"msg"` Data interface {} `json:"data"` } func Response (r *http.Request, w http.ResponseWriter, resp interface {}, err error ) { if err == nil { r := Body{ Code: 0 , Msg: "ok" , Data: resp, } httpx.WriteJson(w, http.StatusOK, r) return } errCode := uint32 (404 ) errMsg := err.Error() httpx.WriteJson(w, http.StatusBadRequest, Body{ Code: errCode, Msg: errMsg, Data: nil , }) }
生成并编辑模板文件
在 api/handler.tpl 修改
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 package {{.PkgName}}import ( "net/http" "github.com/zeromicro/go-zero/rest/httpx" {{.ImportPackages}} "test_go_zero/common" ) {{if .HasDoc}}{{.Doc}}{{end}} func {{.HandlerName}}(svcCtx *svc.ServiceContext) http.HandlerFunc { return func (w http.ResponseWriter, r *http.Request) { {{if .HasRequest}}var req types.{{.RequestType}} if err := httpx.Parse(r, &req); err != nil { httpx.ErrorCtx(r.Context(), w, err) return } {{end}}l := {{.LogicName}}.New{{.LogicType}}(r.Context(), svcCtx) {{if .HasResp}}resp, {{end}}err := l.{{.Call}}({{if .HasRequest}}&req{{end}}) {{if .HasResp}}common.Response(r, w, resp, err){{else }}response.Response(r, w, nil , err){{end}} } }
生成代码(cd 到 api 的目录)
1 goctl api go -api test_api_tpl.api -dir .
带配置文件运行
1 go run core.go -f etc/core-api.yaml
JWT 校验 在 api 文件引入 jwt
1 2 3 4 5 6 7 8 9 @server ( prefix: /api/users jwt: Auth ) service users { @handler userInfo post /info (UserInfoRequest) returns (UserInfoResponse) }
引入 jwt 的方法就会自动进行 jwt 校验,我们还需自己编写签发jwt 逻辑。
在 utils/jwt_enter.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 package utilsimport ( "errors" "time" "github.com/golang-jwt/jwt/v4" ) type JwtPayLoad struct { UserID uint `json:"user_id"` Username string `json:"username"` Role int `json:"role"` } type CustomClaims struct { jwt.RegisteredClaims JwtPayLoad } func GenToken (user JwtPayLoad, accessSecret string , expires int64 ) (string , error ) { claims := CustomClaims{ JwtPayLoad: user, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * time.Duration(expires))), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString([]byte (accessSecret)) } func ParseToken (tokenStr string , accessSecret string , expires int64 ) (*CustomClaims, error ) { token, err := jwt.ParseWithClaims(tokenStr, &CustomClaims{}, func (token *jwt.Token) (interface {}, error ) { return []byte (accessSecret), nil }) if err != nil { return nil , err } if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid { return claims, nil } return nil , errors.New("invalid token" ) }
在配置文件配置密钥和过期时间
1 2 3 4 5 6 Name: users Host: 0.0.0.0 Port: 8888 Auth: AccessSecret: abcdefg AccessExpire: 3600
在无需 jwt 的校验方法的 Login 方法里(logic/loginlogic.go)使用 JWT签发
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func (l *LoginLogic) Login(req *types.LoginRequest) (resp *types.LoginResponse, err error ) { secret := l.svcCtx.Config.Auth.AccessSecret expire := l.svcCtx.Config.Auth.AccessExpire token, err := utils.GenToken(utils.JwtPayLoad{ UserID: 0 , Username: req.Name, Role: 1 , }, secret, expire) if err != nil { return nil , err } return &types.LoginResponse{ Token: token, }, nil }
添加鉴权失败的响应
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 func main () { flag.Parse() var c config.Config conf.MustLoad(*configFile, &c) server := rest.MustNewServer(c.RestConf, rest.WithUnauthorizedCallback(JwtUnauthorizedResult)) defer server.Stop() ctx := svc.NewServiceContext(c) handler.RegisterHandlers(server, ctx) fmt.Printf("Starting server at %s:%d...\n" , c.Host, c.Port) server.Start() } func JwtUnauthorizedResult (w http.ResponseWriter, r *http.Request, err error ) { fmt.Println("JwtUnauthorizedResult:" , err) httpx.WriteJson(w, http.StatusOK, common.Body{ Code: 10087 , Msg: "鉴权失败" , Data: nil , }) }
生成 swagger 文档 不推荐,还要起一个 Docker 并挂载目录。尤其加入自定义响应逻辑后,swagger 识别不了
建议直接把 api 文件丢给 AI 让它转换成 openAPI 格式
通过 gorm 连接数据库 引入并配置其他中间件也可参考
引入依赖
1 2 go get gorm.io/gorm go get gorm.io/driver/mysql
在 config/config.go 里定义结构体(与 yaml 对应)
1 2 3 4 5 6 7 8 9 10 11 type Config struct { rest.RestConf Auth struct { AccessSecret string AccessExpire int64 } MySql struct { DataSource string } }
编辑对应的 yaml
1 2 3 4 5 6 7 8 Name: users Host: 0.0.0.0 Port: 8888 Auth: AccessSecret: abcdefgh AccessExpire: 3600 MySql: DataSource: root:root@tcp(127.0.0.1:3306)/zero_db?charset=utf8mb4&parseTime=True&loc=
编写初始化数据库连接逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 func InitDB (MysqlDataSource string ) *gorm.DB { db, err := gorm.Open(mysql.Open(MysqlDataSource), &gorm.Config{}) if err != nil { panic ("Mysql 连接错误" + err.Error()) } fmt.Println("Mysql 连接成功" ) err = db.AutoMigrate(&models.UserModel{}) if err != nil { panic ("自动建表失败" + err.Error()) } return db }
在 svc/servicecontext.go 里将初始化方法暴露出 的DB 指针注入进ServiceContext 结构体,在NewServiceContext 引入这个方法
1 2 3 4 5 6 7 8 9 10 11 12 13 type ServiceContext struct { Config config.Config DB *gorm.DB } func NewServiceContext (c config.Config) *ServiceContext { db := common.InitDB(c.MySql.DataSource) return &ServiceContext{ Config: c, DB: db, } }
Proto 文件 基本使用跑起来 demo 就基本理解了
服务的拆分 把同组的 rpc 服务放到同一个 service 块
1 2 3 4 5 6 7 8 service Rpc { rpc Login(LoginRequest) returns(LoginResponse); rpc Register(RegisterRequest) returns(RegisterResponse); } service Rpc { rpc UserInfo(IdRequest) returns(UserResponse); }
在生成代码加上参数 -m
1 goctl rpc protoc rpc.proto --go_out=/types --go-grpc_out=/types --zrpc_out=. -m
入门篇完