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

验证版本

1
goctl --version

protoc 安装

protoc 是一个用于生成代码的工具,它可以根据 proto 文件生成C++、Java、Python、Go、PHP 等多重语言的代码,而 gRPC 的代码生成还依赖 protoc-gen-goprotoc-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
goctl api new demo

启动服务

在完成上述代码编写后,我们可以通过如下指令启动服务:

1
2
3
4
5
6
# 进入服务目录
$ cd ~/workspace/api/demo
# 整理依赖文件
$ go mod tidy
# 启动 go 程序
$ go run demo.go

gRPC 服务

⬆️同理

1
goctl rpc new demo

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)

  • 现象: s1=http://localhost:2380 but missing from ... 172.17.0.2

  • 原因:

    IP 地址冲突。

    • etcd 是分布式系统,对“我是谁”非常敏感。
    • 容器里它觉得自己是 172.17.0.x(Docker 分配的内网 IP),但你强制配置里写的是 localhost
    • etcd 启动自检时发现:“配置里说我是 localhost,但我网卡上明明是 172.x.x.x,不对劲,一定是脑裂了”,于是自杀退出。
  • 解决: 简单粗暴地用 0.0.0.0 这种通配地址,让它不再纠结具体 IP,绕过 Docker 的网络隔离限制。

基本指令

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 //自己的 config 不动
UserRpc rpcclient.Rpc //远程的 Rpc 结构体
}

func NewServiceContext(c config.Config) *ServiceContext {
return &ServiceContext{
Config: c,
UserRpc: rpcclient.NewRpc(zrpc.MustNewClient(c.UserRpc)), // 远程的 Rpc 结构体客户端
}
}

编辑业务方法

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}) //调用远程 RPC
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 common

import (
"net/http"

"github.com/zeromicro/go-zero/rest/httpx"
)

type Body struct {
Code uint32 `json:"code"`
Msg string `json:"msg"`
Data interface{} `json:"data"`
}

// Response 统一响应处理
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,
})
}

生成并编辑模板文件

1
goctl template init 

在 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
// Code scaffolded by goctl. Safe to edit.
// goctl {{.version}}

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 err != nil {
// httpx.ErrorCtx(r.Context(), w, err)
//} else {
// {{if .HasResp}}httpx.OkJsonCtx(r.Context(), w, resp){{else}}httpx.Ok(w){{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 utils

import (
"errors"
"time"

"github.com/golang-jwt/jwt/v4"
)

// JwtPayLoad JWT Payload 结构
type JwtPayLoad struct {
UserID uint `json:"user_id"` // 用户ID
Username string `json:"username"` // 用户名
Role int `json:"role"` // 权限 1-普通用户 2-管理员
}

// CustomClaims 自定义JWT声明
type CustomClaims struct {
jwt.RegisteredClaims
JwtPayLoad
}

// GenToken 生成JWT Token
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))
}

// ParseToken 解析JWT Token
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()
}

// JwtUnauthorizedResult 鉴权失败
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

入门篇完



新ICP备2025018290号-1
本站总访问量