merage go-zero-doc

This commit is contained in:
anqiansong 2021-10-16 23:16:17 +08:00 committed by anqiansong
parent b31059e665
commit 19e981f8f0
368 changed files with 27065 additions and 854 deletions

23
.github/workflows/build.sh vendored Normal file
View File

@ -0,0 +1,23 @@
#!/bin/bash
ACCESSTOKEN=$1
REPO="https://x-access-token:${ACCESSTOKEN}@github.com/zeromicro/go-zero-pages.git"
# git 配置
echo "git基础配置"
git config --global user.name "anqiansong"
git config --global user.email "anqiansong@tal.com"
# push
cd ./go-zero.dev
mkdir ./doc
cd ./doc
echo $PWD
echo "document clone..."
git clone ${REPO}
cd go-zero-pages
rm -rf ./*
cp -rf ../../_book/* .
git add ./*
git commit -m 'update document'
echo "document push..."
git push -f

45
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,45 @@
# 当master有代码提交或者有pr时自动build gitbook并覆盖git@github.com:zeromicro/go-zero.git仓库中内容
# 以实现自动发布文档
name: Document Build
on:
push:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: enter work dir
run: cd go-zero.dev
- name: checkout code
uses: actions/checkout@v2
- name: chmod contributor tool
run: chmod +x ../.github/workflows/contributor-tool
- name: get latest contributors(cn)
run: ../.github/workflows/contributor-tool -f contributor -l zh -o ./cn/contributor.md
- name: get latest contributors(en)
run: ../.github/workflows/contributor-tool -f contributor -l en -o ./en/contributor.md
- name: use node.js
uses: actions/setup-node@v1
with:
node-version: '12.18.1'
- name: install gitbook
run: npm install gitbook-cli -g
- name: gitbook version
run: gitbook --version
- name: gitbook install
run: gitbook install
- name: build
run: gitbook build
- name: delete original index
run: rm -f ./_book/index.html
- name: build index
run: ../.github/workflows/contributor-tool -i ./_book/index.html
- name: chmod
run: chmod +x ./.github/workflows/build.sh
- name: publish
run: ../.github/workflows/build.sh ${{ secrets.ACCESSTOKEN }}
shell: bash

BIN
.github/workflows/contributor-tool vendored Executable file

Binary file not shown.

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2020 好未来技术
Copyright (c) 2020 zeromicro
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -1,5 +0,0 @@
# 熔断机制设计
## 设计目的
* 依赖的服务出现大规模故障时,调用方应该尽可能少调用,降低故障服务的压力,使之尽快恢复服务

View File

@ -1,314 +0,0 @@
# Goctl Model
goctl model 为go-zero下的工具模块中的组件之一目前支持识别mysql ddl进行model层代码生成通过命令行或者idea插件即将支持可以有选择地生成带redis cache或者不带redis cache的代码逻辑。
## 快速开始
* 通过ddl生成
```shell script
goctl model mysql ddl -src="./*.sql" -dir="./sql/model" -c=true
```
执行上述命令后即可快速生成CURD代码。
```Plain Text
model
│   ├── error.go
│   └── usermodel.go
```
* 通过datasource生成
```shell script
goctl model mysql datasource -url="user:password@tcp(127.0.0.1:3306)/database" -table="*" -dir="./model"
```
* 生成代码示例
```go
package model
import (
"database/sql"
"fmt"
"strings"
"time"
"github.com/tal-tech/go-zero/core/stores/cache"
"github.com/tal-tech/go-zero/core/stores/sqlc"
"github.com/tal-tech/go-zero/core/stores/sqlx"
"github.com/tal-tech/go-zero/core/stringx"
"github.com/tal-tech/go-zero/tools/goctl/model/sql/builderx"
)
var (
userFieldNames = builderx.FieldNames(&User{})
userRows = strings.Join(userFieldNames, ",")
userRowsExpectAutoSet = strings.Join(stringx.Remove(userFieldNames, "id", "create_time", "update_time"), ",")
userRowsWithPlaceHolder = strings.Join(stringx.Remove(userFieldNames, "id", "create_time", "update_time"), "=?,") + "=?"
cacheUserIdPrefix = "cache#User#id#"
cacheUserNamePrefix = "cache#User#name#"
cacheUserMobilePrefix = "cache#User#mobile#"
)
type (
UserModel struct {
sqlc.CachedConn
table string
}
User struct {
Id int64 `db:"id"`
Name string `db:"name"` // 用户名称
Password string `db:"password"` // 用户密码
Mobile string `db:"mobile"` // 手机号
Gender string `db:"gender"` // 男|女|未公开
Nickname string `db:"nickname"` // 用户昵称
CreateTime time.Time `db:"create_time"`
UpdateTime time.Time `db:"update_time"`
}
)
func NewUserModel(conn sqlx.SqlConn, c cache.CacheConf) *UserModel {
return &UserModel{
CachedConn: sqlc.NewConn(conn, c),
table: "user",
}
}
func (m *UserModel) Insert(data User) (sql.Result, error) {
userNameKey := fmt.Sprintf("%s%v", cacheUserNamePrefix, data.Name)
userMobileKey := fmt.Sprintf("%s%v", cacheUserMobilePrefix, data.Mobile)
ret, err := m.Exec(func(conn sqlx.SqlConn) (result sql.Result, err error) {
query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?)", m.table, userRowsExpectAutoSet)
return conn.Exec(query, data.Name, data.Password, data.Mobile, data.Gender, data.Nickname)
}, userNameKey, userMobileKey)
return ret, err
}
func (m *UserModel) FindOne(id int64) (*User, error) {
userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, id)
var resp User
err := m.QueryRow(&resp, userIdKey, func(conn sqlx.SqlConn, v interface{}) error {
query := fmt.Sprintf("select %s from %s where id = ? limit 1", userRows, m.table)
return conn.QueryRow(v, query, id)
})
switch err {
case nil:
return &resp, nil
case sqlc.ErrNotFound:
return nil, ErrNotFound
default:
return nil, err
}
}
func (m *UserModel) FindOneByName(name string) (*User, error) {
userNameKey := fmt.Sprintf("%s%v", cacheUserNamePrefix, name)
var resp User
err := m.QueryRowIndex(&resp, userNameKey, m.formatPrimary, func(conn sqlx.SqlConn, v interface{}) (i interface{}, e error) {
query := fmt.Sprintf("select %s from %s where name = ? limit 1", userRows, m.table)
if err := conn.QueryRow(&resp, query, name); err != nil {
return nil, err
}
return resp.Id, nil
}, m.queryPrimary)
switch err {
case nil:
return &resp, nil
case sqlc.ErrNotFound:
return nil, ErrNotFound
default:
return nil, err
}
}
func (m *UserModel) FindOneByMobile(mobile string) (*User, error) {
userMobileKey := fmt.Sprintf("%s%v", cacheUserMobilePrefix, mobile)
var resp User
err := m.QueryRowIndex(&resp, userMobileKey, m.formatPrimary, func(conn sqlx.SqlConn, v interface{}) (i interface{}, e error) {
query := fmt.Sprintf("select %s from %s where mobile = ? limit 1", userRows, m.table)
if err := conn.QueryRow(&resp, query, mobile); err != nil {
return nil, err
}
return resp.Id, nil
}, m.queryPrimary)
switch err {
case nil:
return &resp, nil
case sqlc.ErrNotFound:
return nil, ErrNotFound
default:
return nil, err
}
}
func (m *UserModel) Update(data User) error {
userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, data.Id)
_, err := m.Exec(func(conn sqlx.SqlConn) (result sql.Result, err error) {
query := fmt.Sprintf("update %s set %s where id = ?", m.table, userRowsWithPlaceHolder)
return conn.Exec(query, data.Name, data.Password, data.Mobile, data.Gender, data.Nickname, data.Id)
}, userIdKey)
return err
}
func (m *UserModel) Delete(id int64) error {
data, err := m.FindOne(id)
if err != nil {
return err
}
userMobileKey := fmt.Sprintf("%s%v", cacheUserMobilePrefix, data.Mobile)
userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, id)
userNameKey := fmt.Sprintf("%s%v", cacheUserNamePrefix, data.Name)
_, err = m.Exec(func(conn sqlx.SqlConn) (result sql.Result, err error) {
query := fmt.Sprintf("delete from %s where id = ?", m.table)
return conn.Exec(query, id)
}, userMobileKey, userIdKey, userNameKey)
return err
}
func (m *UserModel) formatPrimary(primary interface{}) string {
return fmt.Sprintf("%s%v", cacheUserIdPrefix, primary)
}
func (m *UserModel) queryPrimary(conn sqlx.SqlConn, v, primary interface{}) error {
query := fmt.Sprintf("select %s from %s where id = ? limit 1", userRows, m.table)
return conn.QueryRow(v, query, primary)
}
```
## 用法
```Plain Text
goctl model mysql -h
```
```Plain Text
NAME:
goctl model mysql - generate mysql model"
USAGE:
goctl model mysql command [command options] [arguments...]
COMMANDS:
ddl generate mysql model from ddl"
datasource generate model from datasource"
OPTIONS:
--help, -h show help
```
## 生成规则
* 默认规则
我们默认用户在建表时会创建createTime、updateTime字段(忽略大小写、下划线命名风格)且默认值均为`CURRENT_TIMESTAMP`而updateTime支持`ON UPDATE CURRENT_TIMESTAMP`,对于这两个字段生成`insert`、`update`时会被移除,不在赋值范畴内,当然,如果你不需要这两个字段那也无大碍。
* 带缓存模式
* ddl
```shell script
goctl model mysql -src={patterns} -dir={dir} -cache=true
```
help
```
NAME:
goctl model mysql ddl - generate mysql model from ddl
USAGE:
goctl model mysql ddl [command options] [arguments...]
OPTIONS:
--src value, -s value the path or path globbing patterns of the ddl
--dir value, -d value the target dir
--style value the file naming style, lower|camel|underline,default is lower
--cache, -c generate code with cache [optional]
--idea for idea plugin [optional]
```
* datasource
```shell script
goctl model mysql datasource -url={datasource} -table={patterns} -dir={dir} -cache=true
```
help
```
NAME:
goctl model mysql datasource - generate model from datasource
USAGE:
goctl model mysql datasource [command options] [arguments...]
OPTIONS:
--url value the data source of database,like "root:password@tcp(127.0.0.1:3306)/database
--table value, -t value the table or table globbing patterns in the database
--cache, -c generate code with cache [optional]
--dir value, -d value the target dir
--style value the file naming style, lower|camel|snake, default is lower
--idea for idea plugin [optional]
```
示例用法请参考[用法](./example/generator.sh)
> NOTE: goctl model mysql ddl/datasource 均新增了一个`--style`参数,用于标记文件命名风格。
目前仅支持redis缓存如果选择带缓存模式即生成的`FindOne(ByXxx)`&`Delete`代码会生成带缓存逻辑的代码目前仅支持单索引字段除全文索引外对于联合索引我们默认认为不需要带缓存且不属于通用型代码因此没有放在代码生成行列如example中user表中的`id`、`name`、`mobile`字段均属于单字段索引。
* 不带缓存模式
* ddl
```shell script
goctl model -src={patterns} -dir={dir}
```
* datasource
```shell script
goctl model mysql datasource -url={datasource} -table={patterns} -dir={dir}
```
or
* ddl
```shell script
goctl model -src={patterns} -dir={dir} -cache=false
```
* datasource
```shell script
goctl model mysql datasource -url={datasource} -table={patterns} -dir={dir} -cache=false
```
生成代码仅基本的CURD结构。
## 缓存
对于缓存这一块我选择用一问一答的形式进行罗列。我想这样能够更清晰的描述model中缓存的功能。
* 缓存会缓存哪些信息?
对于主键字段缓存,会缓存整个结构体信息,而对于单索引字段(除全文索引)则缓存主键字段值。
* 数据有更新(`update`)操作会清空缓存吗?
但仅清空主键缓存的信息why这里就不做详细赘述了。
* 为什么不按照单索引字段生成`updateByXxx`和`deleteByXxx`的代码?
理论上是没任何问题但是我们认为对于model层的数据操作均是以整个结构体为单位包括查询我不建议只查询某部分字段不反对否则我们的缓存就没有意义了。
* 为什么不支持`findPageLimit`、`findAll`这么模式代码生层?
目前我认为除了基本的CURD外其他的代码均属于<i>业务型</i>代码,这个我觉得开发人员根据业务需要进行编写更好。

View File

@ -1,244 +0,0 @@
# goctl使用
## goctl用途
* 定义api请求
* 根据定义的api自动生成golang(后端), java(iOS & Android), typescript(web & 小程序)dart(flutter)
* 生成MySQL CURD+Cache
* 生成MongoDB CURD+Cache
## goctl使用说明
### 快速生成服务
* api: goctl api new xxxx
* rpc: goctl rpc new xxxx
#### goctl参数说明
`goctl api [go/java/ts] [-api user/user.api] [-dir ./src]`
> api 后面接生成的语言现支持go/java/typescript
>
> -api 自定义api所在路径
>
> -dir 自定义生成目录
如需自定义模板,运行如下命令生成`api gateway`模板:
```shell
goctl api go template
```
生成的模板放在`$HOME/.goctl`目录下,根据需要自行修改模板,下次运行`goctl`生成代码时会优先采用模板文件的内容
#### API 语法说明
``` golang
info(
title: doc title
desc: >
doc description first part,
doc description second part<
version: 1.0
)
type int userType
type user {
name string `json:"user"` // 用户姓名
}
type student {
name string `json:"name"` // 学生姓名
}
type teacher {
}
type (
address {
city string `json:"city"`
}
innerType {
image string `json:"image"`
}
createRequest {
innerType
name string `form:"name"`
age int `form:"age,optional"`
address []address `json:"address,optional"`
}
getRequest {
name string `path:"name"`
age int `form:"age,optional"`
}
getResponse {
code int `json:"code"`
desc string `json:"desc,omitempty"`
address address `json:"address"`
service int `json:"service"`
}
)
service user-api {
@doc(
summary: user title
desc: >
user description first part,
user description second part,
user description second line
)
@server(
handler: GetUserHandler
group: user
)
get /api/user/:name(getRequest) returns(getResponse)
@server(
handler: CreateUserHandler
group: user
)
post /api/users/create(createRequest)
}
@server(
jwt: Auth
group: profile
)
service user-api {
@doc(summary: user title)
@handler GetProfileHandler
get /api/profile/:name(getRequest) returns(getResponse)
@handler CreateProfileHandler
post /api/profile/create(createRequest)
}
service user-api {
@doc(summary: desc in one line)
@handler PingHandler
head /api/ping()
}
```
1. info部分描述了api基本信息比如Authapi是哪个用途。
2. type部分type类型声明和golang语法兼容。
3. service部分
* service代表一组服务一个服务可以由多组名称相同的service组成可以针对每一组service配置jwt和auth认证。
* 通过group属性可以指定service生成所在子目录。
* service里面包含api路由比如上面第一组service的第一个路由doc用来描述此路由的用途GetProfileHandler表示处理这个路由的handler
`get /api/profile/:name(getRequest) returns(getResponse)` 中get代表api的请求方式get/post/put/delete, `/api/profile/:name` 描述了路由path`:name`通过
请求getRequest里面的属性赋值getResponse为返回的结构体这两个类型都定义在2描述的类型中。
* server 标签支持配置middleware示例如下
```go
@server(
middleware: AuthUser
)
```
添加完middleware后需要设置ServiceContext 中middleware变量的值middleware实现可以参考测试用例 `TestWithMiddleware` 或者 `TestMultiMiddlewares`
* handler 支持缩写,实例如下:
```golang
@handler CreateProfileHandler
post /api/profile/create(createRequest)
```
4. 支持在info下面和type顶部import外部api文件被import的文件只支持类型定义import语法` import xxxx.api `
#### goland/vscode插件
开发者可以在 goland 或 vscode 中搜索 goctl 的 api 插件,它们提供了 api 语法高亮,语法检测和格式化相关功能,插件安装及使用相关资料请点击[这里](https://github.com/tal-tech/goctl-plugins)。
插件支持:
1. 语法高亮和类型导航。
2. 语法检测,格式化 api 会自动检测 api 编写错误地方。
3. api 文档格式化( vscode 默认快捷键 `option+command+f`, goland 默认快捷键 `option+command+l`)。
4. 上下文菜单goland 插件提供了生成代码的快捷菜单。
#### 根据定义好的api文件生成golang代码
命令如下:
`goctl api go -api user/user.api -dir user`
```Plain Text
.
├── internal
│   ├── config
│   │   └── config.go
│   ├── handler
│   │   ├── pinghandler.go
│   │   ├── profile
│   │   │   ├── createprofilehandler.go
│   │   │   └── getprofilehandler.go
│   │   ├── routes.go
│   │   └── user
│   │   ├── createuserhandler.go
│   │   └── getuserhandler.go
│   ├── logic
│   │   ├── pinglogic.go
│   │   ├── profile
│   │   │   ├── createprofilelogic.go
│   │   │   └── getprofilelogic.go
│   │   └── user
│   │   ├── createuserlogic.go
│   │   └── getuserlogic.go
│   ├── svc
│   │   └── servicecontext.go
│   └── types
│   └── types.go
└── user.go
```
生成的代码可以直接跑,有几个地方需要改:
* 在`servicecontext.go`里面增加需要传递给logic的一些资源比如mysql, redisrpc等
* 在定义的get/post/put/delete等请求的handler和logic里增加处理业务逻辑的代码
#### 根据定义好的api文件生成java代码
```shell
goctl api java -api user/user.api -dir ./src
```
#### 根据定义好的api文件生成typescript代码
```shell
goctl api ts -api user/user.api -dir ./src -webapi ***
ts需要指定webapi所在目录
```
#### 根据定义好的api文件生成Dart代码
```shell
goctl api dart -api user/user.api -dir ./src
```
## 根据mysql ddl或者datasource生成model文件
```shell script
goctl model mysql -src={filename} -dir={dir} -c
```
详情参考[model文档](goctl-model-sql.md)
## goctl rpc生成
见[goctl rpc](goctl-rpc.md)

View File

@ -1,136 +0,0 @@
# 基于go-zero实现JWT认证
关于JWT是什么大家可以看看[官网](https://jwt.io/),一句话介绍下:是可以实现服务器无状态的鉴权认证方案,也是目前最流行的跨域认证解决方案。
要实现JWT认证我们需要分成如下两个步骤
* 客户端获取JWT token。
* 服务器对客户端带来的JWT token认证。
## 1. 客户端获取JWT Token
我们定义一个协议供客户端调用获取JWT token我们新建一个目录jwt然后在目录中执行 `goctl api -o jwt.api`将生成的jwt.api改成如下
````go
type JwtTokenRequest struct {
}
type JwtTokenResponse struct {
AccessToken string `json:"access_token"`
AccessExpire int64 `json:"access_expire"`
RefreshAfter int64 `json:"refresh_after"` // 建议客户端刷新token的绝对时间
}
type GetUserRequest struct {
UserId string `json:"userId"`
}
type GetUserResponse struct {
Name string `json:"name"`
}
service jwt-api {
@handler JwtHandler
post /user/token(JwtTokenRequest) returns (JwtTokenResponse)
}
@server(
jwt: JwtAuth
)
service jwt-api {
@handler GetUserHandler
post /user/info(GetUserRequest) returns (GetUserResponse)
}
````
在服务jwt目录中执行`goctl api go -api jwt.api -dir .`
打开jwtlogic.go文件修改 `func (l *JwtLogic) Jwt(req types.JwtTokenRequest) (*types.JwtTokenResponse, error) {` 方法如下:
```go
func (l *JwtLogic) Jwt(req types.JwtTokenRequest) (*types.JwtTokenResponse, error) {
var accessExpire = l.svcCtx.Config.JwtAuth.AccessExpire
now := time.Now().Unix()
accessToken, err := l.GenToken(now, l.svcCtx.Config.JwtAuth.AccessSecret, nil, accessExpire)
if err != nil {
return nil, err
}
return &types.JwtTokenResponse{
AccessToken: accessToken,
AccessExpire: now + accessExpire,
RefreshAfter: now + accessExpire/2,
}, nil
}
func (l *JwtLogic) GenToken(iat int64, secretKey string, payloads map[string]interface{}, seconds int64) (string, error) {
claims := make(jwt.MapClaims)
claims["exp"] = iat + seconds
claims["iat"] = iat
for k, v := range payloads {
claims[k] = v
}
token := jwt.New(jwt.SigningMethodHS256)
token.Claims = claims
return token.SignedString([]byte(secretKey))
}
```
在启动服务之前我们需要修改etc/jwt-api.yaml文件如下
```yaml
Name: jwt-api
Host: 0.0.0.0
Port: 8888
JwtAuth:
AccessSecret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
AccessExpire: 604800
```
启动服务器然后测试下获取到的token。
```sh
➜ curl --location --request POST '127.0.0.1:8888/user/token'
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MDEyNjE0MjksImlhdCI6MTYwMDY1NjYyOX0.6u_hpE_4m5gcI90taJLZtvfekwUmjrbNJ-5saaDGeQc","access_expire":1601261429,"refresh_after":1600959029}
```
## 2. 服务器验证JWT token
1. 在api文件中通过`jwt: JwtAuth`标记的service表示激活了jwt认证。
2. 可以阅读rest/handler/authhandler.go文件了解服务器jwt实现。
3. 修改getuserlogic.go如下
```go
func (l *GetUserLogic) GetUser(req types.GetUserRequest) (*types.GetUserResponse, error) {
return &types.GetUserResponse{Name: "kim"}, nil
}
```
* 我们先不带JWT Authorization header请求头测试下返回http status code是401符合预期。
```sh
➜ curl -w "\nhttp: %{http_code} \n" --location --request POST '127.0.0.1:8888/user/info' \
--header 'Content-Type: application/json' \
--data-raw '{
"userId": "a"
}'
http: 401
```
* 加上Authorization header请求头测试。
```sh
➜ curl -w "\nhttp: %{http_code} \n" --location --request POST '127.0.0.1:8888/user/info' \
--header 'Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MDEyNjE0MjksImlhdCI6MTYwMDY1NjYyOX0.6u_hpE_4m5gcI90taJLZtvfekwUmjrbNJ-5saaDGeQc' \
--header 'Content-Type: application/json' \
--data-raw '{
"userId": "a"
}'
{"name":"kim"}
http: 200
```
综上所述基于go-zero的JWT认证完成在真实生产环境部署时候AccessSecret, AccessExpire, RefreshAfter根据业务场景通过配置文件配置RefreshAfter 是告诉客户端什么时候该刷新JWT token了一般都需要设置过期时间前几天。

View File

@ -1,15 +0,0 @@
# PeriodicalExecutor设计
## 添加任务
* 当前没有未执行的任务
* 添加并启动定时器
* 已有未执行的任务
* 添加并检查是否到达最大缓存数
* 如到,执行所有缓存任务
* 未到,只添加
## 定时器到期
* 清除并执行所有缓存任务
* 再等待N个定时周期如果等待过程中一直没有新任务则退出

View File

@ -1,3 +1,5 @@
# DEPRECATED: PLEASE MOVE TO https://go-zero.dev/cn/extended-reading.html
# Rapid development of microservices
English | [简体中文](shorturl.md)
@ -23,7 +25,7 @@ For any point listed above, we need a long article to describe the theory and th
As well, we always adhere to the idea that **prefer tools over conventions and documents**. We hope to reduce the boilerplate code as much as possible, and let developers focus on developing the business related code. For this purpose, we developed the tool `goctl`.
Lets take the shorturl microservice as a quick example to demonstrate how to quickly create microservices by using [go-zero](https://github.com/tal-tech/go-zero). After finishing this tutorial, youll find that its so easy to write microservices!
Lets take the shorturl microservice as a quick example to demonstrate how to quickly create microservices by using [go-zero](https://github.com/zeromicro/go-zero). After finishing this tutorial, youll find that its so easy to write microservices!
## 1. What is a shorturl service

View File

@ -1,3 +1,5 @@
# DEPRECATED: PLEASE MOVE TO https://go-zero.dev/cn/extended-reading.html
# 快速构建高并发微服务
[English](shorturl-en.md) | 简体中文
@ -23,7 +25,7 @@
另外,我们始终秉承 **工具大于约定和文档** 的理念。我们希望尽可能减少开发人员的心智负担,把精力都投入到产生业务价值的代码上,减少重复代码的编写,所以我们开发了 `goctl` 工具。
下面我通过短链微服务来演示通过 [go-zero](https://github.com/tal-tech/go-zero) 快速的创建微服务的流程,走完一遍,你就会发现:原来编写微服务如此简单!
下面我通过短链微服务来演示通过 [go-zero](https://github.com/zeromicro/go-zero) 快速的创建微服务的流程,走完一遍,你就会发现:原来编写微服务如此简单!
## 1. 什么是短链服务

View File

@ -1,3 +1,4 @@
# DEPRECATED: PLEASE MOVE TO https://go-zero.dev
---
home: true
heroImage: /logo.png

View File

@ -9,10 +9,10 @@
## 代码结构
- [spancontext](https://github.com/tal-tech/go-zero/blob/master/core/trace/spancontext.go)保存链路的上下文信息「traceidspanid或者是其他想要传递的内容」
- [span](https://github.com/tal-tech/go-zero/blob/master/core/trace/span.go):链路中的一个操作,存储时间和某些信息
- [propagator](https://github.com/tal-tech/go-zero/blob/master/core/trace/propagator.go) `trace` 传播下游的操作「抽取,注入」
- [noop](https://github.com/tal-tech/go-zero/blob/master/core/trace/noop.go):实现了空的 `tracer` 实现
- [spancontext](https://github.com/zeromicro/go-zero/blob/master/core/trace/spancontext.go)保存链路的上下文信息「traceidspanid或者是其他想要传递的内容」
- [span](https://github.com/zeromicro/go-zero/blob/master/core/trace/span.go):链路中的一个操作,存储时间和某些信息
- [propagator](https://github.com/zeromicro/go-zero/blob/master/core/trace/propagator.go) `trace` 传播下游的操作「抽取,注入」
- [noop](https://github.com/zeromicro/go-zero/blob/master/core/trace/noop.go):实现了空的 `tracer` 实现
@ -63,7 +63,7 @@ type Span struct {
## 实例应用
`go-zero` 中httprpc中已经作为内置中间件集成。我们以 [http](https://github.com/tal-tech/go-zero/blob/master/rest/handler/tracinghandler.go)[rpc](https://github.com/tal-tech/go-zero/blob/master/zrpc/internal/clientinterceptors/tracinginterceptor.go) 中,看看 `tracing` 是怎么使用的:
`go-zero` 中httprpc中已经作为内置中间件集成。我们以 [http](https://github.com/zeromicro/go-zero/blob/master/rest/handler/tracinghandler.go)[rpc](https://github.com/zeromico/go-zero/blob/master/zrpc/internal/clientinterceptors/tracinginterceptor.go) 中,看看 `tracing` 是怎么使用的:
### HTTP
@ -195,7 +195,7 @@ func StartClientSpan(ctx context.Context, serviceName, operationName string) (co
## 参考
- [go-zero trace](https://github.com/tal-tech/go-zero/tree/master/core/trace)
- [go-zero trace](https://github.com/zeromicro/go-zero/tree/master/core/trace)
- [https://zhuanlan.zhihu.com/p/34318538](https://zhuanlan.zhihu.com/p/34318538)

View File

@ -5,7 +5,7 @@
go-zero微服务框架中提供了许多开箱即用的工具好的工具不仅能提升服务的性能而且还能提升代码的鲁棒性避免出错实现代码风格的统一方便他人阅读等等本系列文章将分别介绍go-zero框架中工具的使用及其实现原理
## 布隆过滤器[bloom](https://github.com/tal-tech/go-zero/blob/master/core/bloom/bloom.go)
## 布隆过滤器[bloom](https://github.com/zeromicro/go-zero/blob/master/core/bloom/bloom.go)
在做服务器开发的时候,相信大家有听过布隆过滤器,可以判断某元素在不在集合里面,因为存在一定的误判和删除复杂问题,一般的使用场景是:防止缓存击穿(防止恶意攻击)、 垃圾邮箱过滤、cache digests 、模型检测器等、判断是否存在某行数据,用以减少对磁盘访问,提高服务的访问性能。     go-zero 提供的简单的缓存封装 bloom.bloom简单使用方式如下

View File

@ -340,7 +340,7 @@ func (pe *PeriodicalExecutor) Wait() {
- 在分析 `confirmChan` 发现,在此次[提交](https://github.com/tal-tech/go-zero/commit/9d9399ad1014c171cc9bd9c87f78b5d2ac238ce4)才出现,为什么会这么设计?
- 在分析 `confirmChan` 发现,在此次[提交](https://github.com/zeromicro/go-zero/commit/9d9399ad1014c171cc9bd9c87f78b5d2ac238ce4)才出现,为什么会这么设计?

View File

@ -62,7 +62,7 @@ USAGE:
OPTIONS:
--src value, -s value the path or path globbing patterns of the ddl
--dir value, -d value the target dir
--style value the file naming format, see [https://github.com/tal-tech/go-zero/tree/master/tools/goctl/config/readme.md]
--style value the file naming format, see [https://github.com/zeromicro/go-zero/tree/master/tools/goctl/config/readme.md]
--cache, -c generate code with cache [optional]
--idea for idea plugin [optional]
```
@ -70,7 +70,7 @@ OPTIONS:
- `--src`指定sql文件名含路径支持相对路径支持通配符匹配
- `--dir`:指定代码存放的目标文件夹
- `--style`:指定生成文件名命名方式,参考[config](https://github.com/tal-tech/go-zero/blob/master/tools/goctl/config/readme.md)
- `--style`:指定生成文件名命名方式,参考[config](https://github.com/zeromicro/go-zero/blob/master/tools/goctl/config/readme.md)
- `--cache`指定缓存方式true生成带redis缓存代码false生成不带redis缓存代码默认false
- `--idea`:略
@ -115,7 +115,7 @@ OPTIONS:
--table value, -t value the table or table globbing patterns in the database
--cache, -c generate code with cache [optional]
--dir value, -d value the target dir
--style value the file naming format, see [https://github.com/tal-tech/go-zero/tree/master/tools/goctl/config/readme.md]
--style value the file naming format, see [https://github.com/zeromicro/go-zero/tree/master/tools/goctl/config/readme.md]
--idea for idea plugin [optional]
```
@ -124,7 +124,7 @@ OPTIONS:
- `--url`:指定数据库连接地址,如`user:password@tcp(127.0.0.1:3306)/gozero`
- `--table`:指定表名,支持通配符匹配,即匹配`gozero`数据库中的表
- `--dir`:指定代码存放的目标文件夹
- `--style`:指定生成文件名命名方式,参考[config](https://github.com/tal-tech/go-zero/blob/master/tools/goctl/config/readme.md)
- `--style`:指定生成文件名命名方式,参考[config](https://github.com/zeromicro/go-zero/blob/master/tools/goctl/config/readme.md)
- `--cache`指定缓存方式true生成带redis缓存代码false生成不带redis缓存代码默认false
- `--idea`:略

View File

@ -3,7 +3,7 @@
# goctl概述
goctl是[go-zero](https://github.com/tal-tech/go-zero)微服务框架下的代码生成工具其可以快速提升开发效率让开发人员将时间重点放在业务coding上其具体功能如下
goctl是[go-zero](https://github.com/zeromico/go-zero)微服务框架下的代码生成工具其可以快速提升开发效率让开发人员将时间重点放在业务coding上其具体功能如下
- [api服务生成](https://www.yuque.com/tal-tech/go-zero/ppnpng)
@ -66,7 +66,7 @@ $ GO111MODULE=on GOPROXY=https://goproxy.cn/,direct go get -u github.com/tal-tec
### 方式二 fork and build
从[go-zero](https://github.com/tal-tech/go-zero)拉取一份go-zero源码`git@github.com:tal-tech/go-zero.git`进入goctl`tools/goctl/`目录下编译一下goctl文件然后将其添加到环境变量中。
从[go-zero](https://github.com/zeromicro/go-zero)拉取一份go-zero源码`git@github.com:tal-tech/go-zero.git`进入goctl`tools/goctl/`目录下编译一下goctl文件然后将其添加到环境变量中。
# 校验

View File

@ -9,8 +9,8 @@ $ goctl api plugin -p goctl-android="android -package com.tal" -api user.api -di
上面这个命令可以分解成如下几部:
1. goctl 解析api文件
1. goctl 将解析后的结构 [ApiSpec](https://github.com/tal-tech/go-zero/blob/16bfb1b7be2db014348b6be9a0e0abe0f765cd38/tools/goctl/api/spec/spec.go) 和参数传递给goctl-android可执行文件
1. goctl-android 根据 [ApiSpec](https://github.com/tal-tech/go-zero/blob/16bfb1b7be2db014348b6be9a0e0abe0f765cd38/tools/goctl/api/spec/spec.go) 结构体自定义生成逻辑。
1. goctl 将解析后的结构 [ApiSpec](https://github.com/zeromicro/go-zero/blob/16bfb1b7be2db014348b6be9a0e0abe0f765cd38/tools/goctl/api/spec/spec.go) 和参数传递给goctl-android可执行文件
1. goctl-android 根据 [ApiSpec](https://github.com/zeromicro/go-zero/blob/16bfb1b7be2db014348b6be9a0e0abe0f765cd38/tools/goctl/api/spec/spec.go) 结构体自定义生成逻辑。
@ -18,7 +18,7 @@ $ goctl api plugin -p goctl-android="android -package com.tal" -api user.api -di
## 怎么编写自定义插件?
go-zero框架中包含了一个很简单的自定义插件 [demo](https://github.com/tal-tech/go-zero/blob/master/tools/goctl/plugin/demo/goctlplugin.go),代码如下:
go-zero框架中包含了一个很简单的自定义插件 [demo](https://github.com/zeromicro/go-zero/blob/master/tools/goctl/plugin/demo/goctlplugin.go),代码如下:
```go
package main

View File

@ -45,7 +45,7 @@ USAGE:
goctl rpc new [command options] [arguments...]
OPTIONS:
--style value the file naming format, see [https://github.com/tal-tech/go-zero/tree/master/tools/goctl/config/readme.md]
--style value the file naming format, see [https://github.com/zeromicro/go-zero/tree/master/tools/goctl/config/readme.md]
--idea whether the command execution environment is from idea plugin. [optional]
```
@ -119,7 +119,7 @@ OPTIONS:
--src value, -s value the file path of the proto source file
--proto_path value, -I value native command of protoc, specify the directory in which to search for imports. [optional]
--dir value, -d value the target path of the code
--style value the file naming format, see [https://github.com/tal-tech/go-zero/tree/master/tools/goctl/config/readme.md]
--style value the file naming format, see [https://github.com/zeromicro/go-zero/tree/master/tools/goctl/config/readme.md]
--idea whether the command execution environment is from idea plugin. [optional]
```

View File

@ -248,7 +248,7 @@ func (p Stream) Transform(fn func(item interface{}) interface{}) Stream {
## 参考资料
- [go-zero](https://github.com/tal-tech/go-zero)
- [go-zero](https://github.com/zeromicro/go-zero)
- [go-zero 文档](https://www.yuque.com/tal-tech/go-zero)
- [Java Stream 详解](https://colobu.com/2016/03/02/Java-Stream/)
- [Java 8中Stream API](https://mp.weixin.qq.com/s/xa98C-QUHRUK0BhWLzI3XQ)

View File

@ -276,7 +276,7 @@ func (tw *TimingWheel) getPositionAndCircle(d time.Duration) (pos int, circle in
## 参考资料
- [go-zero](https://github.com/tal-tech/go-zero)
- [go-zero](https://github.com/zeromicro/go-zero)
- [go-zero 文档](https://www.yuque.com/tal-tech/go-zero)
- [go-zero中 collection.Cache](https://github.com/zeromicro/zero-doc/blob/main/doc/collection.md)

View File

@ -156,7 +156,7 @@ return allowed
## 参考
- [go-zero tokenlimit](https://github.com/tal-tech/go-zero/blob/master/core/limit/tokenlimit.go)
- [go-zero tokenlimit](https://github.com/zeromicro/go-zero/blob/master/core/limit/tokenlimit.go)
- [Go-Redis 提供的分布式限流库](https://github.com/go-redis/redis_rate)
<Vssue title="tokenLimit" />

View File

@ -57,7 +57,7 @@ Done.
```
> NOTE:关于api语法请查看[《api语法》](https://www.yuque.com/tal-tech/go-zero/ze9i30)
> NOTE:关于api语法请查看[《api语法》](https://www.yuque.com/zeromicro/go-zero/ze9i30)

View File

@ -5,7 +5,7 @@
流数据处理在我们的日常工作中非常常见,举个例子,我们在业务开发中往往会记录许多业务日志,这些日志一般是先发送到 Kafka然后再由 Job 消费 Kafaka 写到 elasticsearch在进行日志流处理的过程中往往还会对日志做一些处理比如过滤无效的日志做一些计算以及重新组合日志等等示意图如下:
![fx_log.png](https://cdn.nlark.com/yuque/0/2020/png/1220818/1602254714159-7753eb68-5e65-4194-94ad-197af105ed44.png#align=left&display=inline&height=766&margin=%5Bobject%20Object%5D&name=fx_log.png&originHeight=766&originWidth=1422&size=74150&status=done&style=none&width=1422)
### 流处理工具fx
[go-zero](https://github.com/tal-tech/go-zero)是一个功能完备的微服务框架,框架中内置了很多非常实用的工具,其中就包含流数据处理工具[fx](https://github.com/tal-tech/go-zero/tree/master/core/fx),下面我们通过一个简单的例子来认识下该工具:
[go-zero](https://github.com/zeromicro/go-zero)是一个功能完备的微服务框架,框架中内置了很多非常实用的工具,其中就包含流数据处理工具[fx](https://github.com/zeromicro/go-zero/tree/master/core/fx),下面我们通过一个简单的例子来认识下该工具:
```go
package main

2
go-zero.dev/LANGS.md Normal file
View File

@ -0,0 +1,2 @@
* [English](en)
* [中文](cn)

3
go-zero.dev/README-EN.MD Normal file
View File

@ -0,0 +1,3 @@
# go-zero.dev
This directory is the gitbook source for the official document https://go-zero.dev, when you prepare to pull a request
into master, [anqiansong](https://github.com/anqiansong) is one of reviewer must be assigned to. *IMPORT!!!*

3
go-zero.dev/README.MD Normal file
View File

@ -0,0 +1,3 @@
# go-zero.dev
本目录主是官方文档 https://go-zero.dev 的源文档,修改后 pr 请 assign [anqiansong](https://github.com/anqiansong)
进行 review切记

90
go-zero.dev/book.json Normal file
View File

@ -0,0 +1,90 @@
{
"title": "go-zero document",
"author": "anqiansong",
"description": "Golang 微服务框架 | 集成各种工程实践的 WEB 和 RPC 框架 | 一键生成 Go, iOS, Android, Kotlin, Dart, TypeScript, JavaScript 代码",
"language": "zh-hans",
"gitbook": "3.2.3",
"plugins": [
"back-to-top-button",
"chapter-fold",
"code",
"-lunr",
"-search",
"search-pro",
"github",
"splitter",
"-sharing",
"sharing-plus",
"tbfed-pagefooter",
"flexible-alerts",
"page-toc-button",
"pageview-count",
"popup",
"hide-element",
"edit-link",
"-highlight",
"prism",
"theme-comscore"
],
"pluginsConfig": {
"prism": {
"lang": {
"shell": "bash"
},
"css": [
"prismjs/themes/prism-tomorrow.css"
]
},
"github": {
"url": "https://github.com/zeromicro/go-zero"
},
"page-toc-button": {
"maxTocDepth": 2,
"minTocSize": 2
},
"tbfed-pagefooter": {
"copyright": "Copyright © 2019-2021 go-zero",
"modify_label": "Last UpdateTime",
"modify_format": "YYYY-MM-DD HH:mm:ss"
},
"hide-element": {
"elements": [
".gitbook-link"
]
},
"edit-link": {
"base": "https://github.com/zeromicro/zero-doc/tree/main/go-zero.dev",
"label": "EDIT THIS PAGE"
},
"sharing": {
"douban": false,
"facebook": false,
"google": true,
"hatenaBookmark": false,
"instapaper": false,
"line": true,
"linkedin": true,
"messenger": false,
"pocket": false,
"qq": false,
"qzone": true,
"stumbleupon": false,
"twitter": false,
"viber": false,
"vk": false,
"weibo": true,
"whatsapp": false,
"all": [
"douban",
"facebook",
"google",
"linkedin",
"twitter",
"weibo",
"qq",
"qzone",
"weibo"
]
}
}
}

218
go-zero.dev/cn/README.md Normal file
View File

@ -0,0 +1,218 @@
<img align="right" width="150px" src="https://gitee.com/kevwan/static/raw/master/doc/images/go-zero.png">
# go-zero
[![Go](https://github.com/zeromicro/go-zero/workflows/Go/badge.svg?branch=master)](https://github.com/tal-tech/go-zero/actions)
[![Go Report Card](https://goreportcard.com/badge/github.com/tal-tech/go-zero)](https://goreportcard.com/report/github.com/tal-tech/go-zero)
[![goproxy](https://goproxy.cn/stats/github.com/tal-tech/go-zero/badges/download-count.svg)](https://goproxy.cn/stats/github.com/tal-tech/go-zero/badges/download-count.svg)
[![codecov](https://codecov.io/gh/tal-tech/go-zero/branch/master/graph/badge.svg)](https://codecov.io/gh/tal-tech/go-zero)
[![Release](https://img.shields.io/github/v/release/tal-tech/go-zero.svg?style=flat-square)](https://github.com/tal-tech/go-zero)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
## 0. go-zero 介绍
go-zero 是一个集成了各种工程实践的 web 和 rpc 框架。通过弹性设计保障了大并发服务端的稳定性,经受了充分的实战检验。
go-zero 包含极简的 API 定义和生成工具 goctl可以根据定义的 api 文件一键生成 Go, iOS, Android, Kotlin, Dart, TypeScript, JavaScript 代码,并可直接运行。
使用 go-zero 的好处:
* 轻松获得支撑千万日活服务的稳定性
* 内建级联超时控制、限流、自适应熔断、自适应降载等微服务治理能力,无需配置和额外代码
* 微服务治理中间件可无缝集成到其它现有框架使用
* 极简的 API 描述,一键生成各端代码
* 自动校验客户端请求参数合法性
* 大量微服务治理和并发工具包
<img src="https://gitee.com/kevwan/static/raw/master/doc/images/architecture.png" alt="架构图" width="1500" />
## 1. go-zero 框架背景
18 年初,我们决定从 `Java+MongoDB` 的单体架构迁移到微服务架构,经过仔细思考和对比,我们决定:
* 基于 Go 语言
* 高效的性能
* 简洁的语法
* 广泛验证的工程效率
* 极致的部署体验
* 极低的服务端资源成本
* 自研微服务框架
* 有过很多微服务框架自研经验
* 需要有更快速的问题定位能力
* 更便捷的增加新特性
## 2. go-zero 框架设计思考
对于微服务框架的设计,我们期望保障微服务稳定性的同时,也要特别注重研发效率。所以设计之初,我们就有如下一些准则:
* 保持简单,第一原则
* 弹性设计,面向故障编程
* 工具大于约定和文档
* 高可用
* 高并发
* 易扩展
* 对业务开发友好,封装复杂度
* 约束做一件事只有一种方式
我们经历不到半年时间,彻底完成了从 `Java+MongoDB``Golang+MySQL` 为主的微服务体系迁移,并于 18 年 8 月底完全上线,稳定保障了业务后续迅速增长,确保了整个服务的高可用。
## 3. go-zero 项目实现和特点
go-zero 是一个集成了各种工程实践的包含 web 和 rpc 框架,有如下主要特点:
* 强大的工具支持,尽可能少的代码编写
* 极简的接口
* 完全兼容 net/http
* 支持中间件,方便扩展
* 高性能
* 面向故障编程,弹性设计
* 内建服务发现、负载均衡
* 内建限流、熔断、降载,且自动触发,自动恢复
* API 参数自动校验
* 超时级联控制
* 自动缓存控制
* 链路跟踪、统计报警等
* 高并发支撑,稳定保障了疫情期间每天的流量洪峰
如下图,我们从多个层面保障了整体服务的高可用:
![弹性设计](https://gitee.com/kevwan/static/raw/master/doc/images/resilience.jpg)
觉得不错的话,别忘 **star** 👏
## 4. Installation
在项目目录下通过如下命令安装:
```shell
GO111MODULE=on GOPROXY=https://goproxy.cn/,direct go get -u github.com/tal-tech/go-zero
```
## 5. Quick Start
0. 完整示例请查看
[快速构建高并发微服务](https://github.com/tal-tech/zero-doc/blob/main/doc/shorturl.md)
[快速构建高并发微服务 - 多 RPC 版](https://github.com/tal-tech/zero-doc/blob/main/docs/zero/bookstore.md)
1. 安装 goctl 工具
`goctl` 读作 `go control`,不要读成 `go C-T-L`。`goctl` 的意思是不要被代码控制,而是要去控制它。其中的 `go` 不是指 `golang`。在设计 `goctl` 之初,我就希望通过 ` 她 `
来解放我们的双手👈
```shell
GO111MODULE=on GOPROXY=https://goproxy.cn/,direct go get -u github.com/tal-tech/go-zero/tools/goctl
```
如果使用 go1.16 版本, 可以使用 `go install` 命令安装
```shell
GOPROXY=https://goproxy.cn/,direct go install github.com/tal-tech/go-zero/tools/goctl@latest
```
确保 goctl 可执行
2. 快速生成 api 服务
```shell
goctl api new greet
cd greet
go mod init
go mod tidy
go run greet.go -f etc/greet-api.yaml
```
默认侦听在 8888 端口(可以在配置文件里修改),可以通过 curl 请求:
```shell
curl -i http://localhost:8888/from/you
```
返回如下:
```http
HTTP/1.1 200 OK
Content-Type: application/json
Date: Thu, 22 Oct 2020 14:03:18 GMT
Content-Length: 14
{"message":""}
```
编写业务代码:
* api 文件定义了服务对外暴露的路由,可参考 [api 规范](https://github.com/tal-tech/zero-doc/blob/main/doc/goctl.md)
* 可以在 servicecontext.go 里面传递依赖给 logic比如 mysql, redis 等
* 在 api 定义的 get/post/put/delete 等请求对应的 logic 里增加业务处理逻辑
3. 可以根据 api 文件生成前端需要的 Java, TypeScript, Dart, JavaScript 代码
```shell
goctl api java -api greet.api -dir greet
goctl api dart -api greet.api -dir greet
...
```
## 6. Benchmark
![benchmark](https://gitee.com/kevwan/static/raw/master/doc/images/benchmark.png)
[测试代码见这里](https://github.com/smallnest/go-web-framework-benchmark)
## 7. 文档
* [API 文档](api-grammar.md)
* [goctl 使用帮助](goctl.md)
* 常见问题
* 因为 `etcd``grpc` 兼容性问题,请使用 `grpc@v1.29.1`
`google.golang.org/grpc v1.29.1`
* 因为 `protobuf` 兼容性问题,请使用 `protocol-gen@v1.3.2`
`go get -u github.com/golang/protobuf/protoc-gen-go@v1.3.2`
* awesome 系列(更多文章见『微服务实践』公众号)
* [快速构建高并发微服务](https://github.com/tal-tech/zero-doc/blob/main/doc/shorturl.md)
* [快速构建高并发微服务 - 多 RPC 版](https://github.com/tal-tech/zero-doc/blob/main/docs/zero/bookstore.md)
* 精选 `goctl` 插件
<table>
<tr>
<td>插件 </td> <td>用途 </td>
</tr>
<tr>
<td><a href="https://github.com/zeromicro/goctl-swagger">goctl-swagger</a></td> <td>一键生成 <code>api</code><code>swagger</code> 文档 </td>
</tr>
<tr>
<td><a href="https://github.com/zeromicro/goctl-android">goctl-android</a></td> <td> 生成 <code>java (android)</code><code>http client</code> 请求代码</td>
</tr>
<tr>
<td><a href="https://github.com/zeromicro/goctl-go-compact">goctl-go-compact</a> </td> <td>合并 <code>api</code> 里同一个 <code>group</code> 里的 <code>handler</code> 到一个 go 文件</td>
</tr>
</table>
## 8. 微信公众号
`go-zero` 相关文章都会在 `微服务实践` 公众号整理呈现,欢迎扫码关注,也可以通过公众号私信我 👏
<img src="https://zeromicro.github.io/go-zero-pages/resource/go-zero-practise.png" alt="wechat" width="300" />
## 9. 微信交流群
如果文档中未能覆盖的任何疑问,欢迎您在群里提出,我们会尽快答复。
您可以在群内提出使用中需要改进的地方,我们会考虑合理性并尽快修改。
如果您发现 ***bug*** 请及时提 ***issue***,我们会尽快确认并修改。
为了防止广告用户、识别技术同行,请 ***star*** 后加我时注明 **github** 当前 ***star*** 数,我再拉进 **go-zero** 群,感谢!
加我之前有劳点一下 ***star***,一个小小的 ***star*** 是作者们回答海量问题的动力🤝
<img src="https://raw.githubusercontent.com/tal-tech/zero-doc/main/doc/images/wechat.jpg" alt="wechat" width="300" />

View File

@ -0,0 +1,20 @@
# 关于我们
## go-zero
go-zero 是一个集成了各种工程实践的 web 和 rpc 框架。通过弹性设计保障了大并发服务端的稳定性,经受了充分的实战检验。
go-zero 包含极简的 API 定义和生成工具 goctl可以根据定义的 api 文件一键生成 Go, iOS, Android, Kotlin, Dart, TypeScript, JavaScript 代码,并可直接运行。
## go-zero作者
[<img src="https://zeromicro.github.io/go-zero-pages/resource/author.jpeg" width="80px" height="80px" alt="kevwan"/>](https://github.com/kevwan)
**万俊峰**晓黑板研发负责人好未来技术委员会资深专家拥有14年研发团队管理经验16年架构设计经验20年工程实战经验负责过多个大型项目的架构设计曾多次合伙创业被收购ArchSummit全球架构师峰会明星讲师GopherChina大会金牌讲师腾讯云开发者大会讲师。
## go-zero成员
go-zero截止2021年4月目前拥有30人的团队开发人员及60+的社区成员。
## go-zero社区
我们目前拥有4000多人的社区成员在这里你可以和大家讨论任何关于go-zero的技术问题反馈获取最新的go-zero信息以及各位大佬每天分享的技术心得。
## go-zero社区群
<img src="https://raw.githubusercontent.com/tal-tech/zero-doc/main/doc/images/wechat.jpg" width="300" alt="社区群"/>

View File

@ -0,0 +1,53 @@
# api文件编写
## 编写user.api文件
```shell
$ vim service/user/cmd/api/user.api
```
```text
type (
LoginReq {
Username string `json:"username"`
Password string `json:"password"`
}
LoginReply {
Id int64 `json:"id"`
Name string `json:"name"`
Gender string `json:"gender"`
AccessToken string `json:"accessToken"`
AccessExpire int64 `json:"accessExpire"`
RefreshAfter int64 `json:"refreshAfter"`
}
)
service user-api {
@handler login
post /user/login (LoginReq) returns (LoginReply)
}
```
## 生成api服务
### 方式一
```shell
$ cd book/service/user/cmd/api
$ goctl api go -api user.api -dir .
```
```text
Done.
```
### 方式二
`user.api` 文件右键,依次点击进入 `New`->`Go Zero`->`Api Code` 进入目标目录选择即api源码的目标存放目录默认为user.api所在目录选择好目录后点击OK即可。
![api生成](https://zeromicro.github.io/go-zero-pages/resource/goctl-api.png)
![api生成目录选择](https://zeromicro.github.io/go-zero-pages/resource/goctl-api-select.png)
### 方式三
打开user.api进入编辑区,使用快捷键`Command+N`for mac OS或者 `alt+insert`for windows选择`Api Code`同样进入目录选择弹窗选择好目录后点击OK即可。
# 猜你想看
* [api语法](api-grammar.md)
* [goctl api命令](goctl-api.md)
* [api目录结构介绍](api-dir.md)

View File

@ -0,0 +1,110 @@
# api配置
api配置控制着api服务中的各种功能包含但不限于服务监听地址端口环境配置日志配置等下面我们从一个简单的配置来看一下api中常用配置分别有什么作用。
## 配置说明
通过yaml配置我们会发现有很多参数我们并没有于config对齐这是因为config定义中有很多都是带`optional`或者`default`
标签的,对于`optional`可选项,你可以根据自己需求判断是否需要设置,对于`default`标签,如果你觉得默认值就已经够了,可以不用设置,
一般`default`中的值基本不用修改,可以认为是最佳实践值。
### Config
```go
type Config struct{
rest.RestConf // rest api配置
Auth struct { // jwt鉴权配置
AccessSecret string // jwt密钥
AccessExpire int64 // 有效期,单位:秒
}
Mysql struct { // 数据库配置除mysql外可能还有mongo等其他数据库
DataSource string // mysql链接地址满足 $user:$password@tcp($ip:$port)/$db?$queries 格式即可
}
CacheRedis cache.CacheConf // redis缓存
UserRpc zrpc.RpcClientConf // rpc client配置
}
```
### rest.RestConf
api服务基础配置包含监听地址监听端口证书配置限流熔断参数超时参数等控制对其展开我们可以看到
```go
service.ServiceConf // service配置
Host string `json:",default=0.0.0.0"` // http监听ip默认0.0.0.0
Port int // http监听端口,必填
CertFile string `json:",optional"` // https证书文件可选
KeyFile string `json:",optional"` // https私钥文件可选
Verbose bool `json:",optional"` // 是否打印详细http请求日志
MaxConns int `json:",default=10000"` // http同时可接受最大请求数限流数默认10000
MaxBytes int64 `json:",default=1048576,range=[0:8388608]"` // http可接受请求的最大ContentLength默认1048576被设置值不能必须在0到8388608之间
// milliseconds
Timeout int64 `json:",default=3000"` // 超时时长控制单位毫秒默认3000
CpuThreshold int64 `json:",default=900,range=[0:1000]"` // cpu降载阈值默认900可允许设置范围0到1000
Signature SignatureConf `json:",optional"` // 签名配置
```
### service.ServiceConf
```go
type ServiceConf struct {
Name string // 服务名称
Log logx.LogConf // 日志配置
Mode string `json:",default=pro,options=dev|test|pre|pro"` // 服务环境dev-开发环境test-测试环境pre-预发环境pro-正式环境
MetricsUrl string `json:",optional"` // 指标上报接口地址该地址需要支持post json即可
Prometheus prometheus.Config `json:",optional"` // prometheus配置
}
```
### logx.LogConf
```go
type LogConf struct {
ServiceName string `json:",optional"` // 服务名称
Mode string `json:",default=console,options=console|file|volume"` // 日志模式console-输出到consolefile-输出到当前服务器容器文件volume-输出docker挂在文件内
Path string `json:",default=logs"` // 日志存储路径
Level string `json:",default=info,options=info|error|severe"` // 日志级别
Compress bool `json:",optional"` // 是否开启gzip压缩
KeepDays int `json:",optional"` // 日志保留天数
StackCooldownMillis int `json:",default=100"` // 日志write间隔
}
```
### prometheus.Config
```go
type Config struct {
Host string `json:",optional"` // prometheus 监听host
Port int `json:",default=9101"` // prometheus 监听端口
Path string `json:",default=/metrics"` // 上报地址
}
```
### SignatureConf
```go
SignatureConf struct {
Strict bool `json:",default=false"` // 是否Strict模式如果是则PrivateKeys必填
Expiry time.Duration `json:",default=1h"` // 有效期默认1小时
PrivateKeys []PrivateKeyConf // 签名密钥相关配置
}
```
### PrivateKeyConf
```go
PrivateKeyConf struct {
Fingerprint string // 指纹配置
KeyFile string // 密钥配置
}
```
### cache.CacheConf
```go
ClusterConf []NodeConf
NodeConf struct {
redis.RedisConf
Weight int `json:",default=100"` // 权重
}
```
### redis.RedisConf
```go
RedisConf struct {
Host string // redis地址
Type string `json:",default=node,options=node|cluster"` // redis类型
Pass string `json:",optional"` // redis密码
}
```

24
go-zero.dev/cn/api-dir.md Normal file
View File

@ -0,0 +1,24 @@
# api目录介绍
```text
.
├── etc
│ └── greet-api.yaml // 配置文件
├── go.mod // mod文件
├── greet.api // api描述文件
├── greet.go // main函数入口
└── internal
├── config
│ └── config.go // 配置声明type
├── handler // 路由及handler转发
│ ├── greethandler.go
│ └── routes.go
├── logic // 业务逻辑
│ └── greetlogic.go
├── middleware // 中间件文件
│ └── greetmiddleware.go
├── svc // logic所依赖的资源池
│ └── servicecontext.go
└── types // request、response的struct根据api自动生成不建议编辑
└── types.go
```

View File

@ -0,0 +1,733 @@
# api语法介绍
## api示例
```go
/**
* api语法示例及语法说明
*/
// api语法版本
syntax = "v1"
// import literal
import "foo.api"
// import group
import (
"bar.api"
"foo/bar.api"
)
info(
author: "songmeizi"
date: "2020-01-08"
desc: "api语法示例及语法说明"
)
// type literal
type Foo{
Foo int `json:"foo"`
}
// type group
type(
Bar{
Bar int `json:"bar"`
}
)
// service block
@server(
jwt: Auth
group: foo
)
service foo-api{
@doc "foo"
@handler foo
post /foo (Foo) returns (Bar)
}
```
## api语法结构
* syntax语法声明
* import语法块
* info语法块
* type语法块
* service语法块
* 隐藏通道
> [!TIP]
> 在以上语法结构中,各个语法块从语法上来说,按照语法块为单位,可以在.api文件中任意位置声明
> 但是为了提高阅读效率,我们建议按照以上顺序进行声明,因为在将来可能会通过严格模式来控制语法块的顺序。
### syntax语法声明
syntax是新加入的语法结构该语法的引入可以解决
* 快速针对api版本定位存在问题的语法结构
* 针对版本做语法解析
* 防止api语法大版本升级导致前后不能向前兼容
> **[!WARNING]
> 被import的api必须要和main api的syntax版本一致。
**语法定义**
```antlrv4
'syntax'={checkVersion(p)}STRING
```
**语法说明**
syntax固定token标志一个syntax语法结构的开始
checkVersion自定义go方法检测`STRING`是否为一个合法的版本号目前检测逻辑为STRING必须是满足`(?m)"v[1-9][0-9]*"`正则。
STRING一串英文双引号包裹的字符串如"v1"
一个api语法文件只能有0或者1个syntax语法声明如果没有syntax则默认为v1版本
**正确语法示例** ✅
eg1不规范写法
```api
syntax="v1"
```
eg2规范写法(推荐)
```api
syntax = "v2"
```
**错误语法示例** ❌
eg1
```api
syntax = "v0"
```
eg2
```api
syntax = v1
```
eg3
```api
syntax = "V1"
```
## import语法块
随着业务规模增大api中定义的结构体和服务越来越多所有的语法描述均为一个api文件这是多么糟糕的一个问题 其会大大增加了阅读难度和维护难度import语法块可以帮助我们解决这个问题通过拆分api文件
不同的api文件按照一定规则声明可以降低阅读难度和维护难度。
> **[!WARNING]
> 这里import不像golang那样包含package声明仅仅是一个文件路径的引入最终解析后会把所有的声明都汇聚到一个spec.Spec中。
> 不能import多个相同路径否则会解析错误。
**语法定义**
```antlrv4
'import' {checkImportValue(p)}STRING
|'import' '(' ({checkImportValue(p)}STRING)+ ')'
```
**语法说明**
import固定token标志一个import语法的开始
checkImportValue自定义go方法检测`STRING`是否为一个合法的文件路径目前检测逻辑为STRING必须是满足`(?m)"(/?[a-zA-Z0-9_#-])+\.api"`正则。
STRING一串英文双引号包裹的字符串如"foo.api"
**正确语法示例** ✅
eg
```api
import "foo.api"
import "foo/bar.api"
import(
"bar.api"
"foo/bar/foo.api"
)
```
**错误语法示例** ❌
eg
```api
import foo.api
import "foo.txt"
import (
bar.api
bar.api
)
```
## info语法块
info语法块是一个包含了多个键值对的语法体其作用相当于一个api服务的描述解析器会将其映射到spec.Spec中 以备用于翻译成其他语言(golang、java等)
时需要携带的meta元素。如果仅仅是对当前api的一个说明而不考虑其翻译 时传递到其他语言则使用简单的多行注释或者java风格的文档注释即可关于注释说明请参考下文的 **隐藏通道**
> **[!WARNING]
> 不能使用重复的key每个api文件只能有0或者1个info语法块
**语法定义**
```antlrv4
'info' '(' (ID {checkKeyValue(p)}VALUE)+ ')'
```
**语法说明**
info固定token标志一个info语法块的开始
checkKeyValue自定义go方法检测`VALUE`是否为一个合法值。
VALUEkey对应的值可以为单行的除'\r','\n','/'后的任意字符,多行请以""包裹,不过强烈建议所有都以""包裹
**正确语法示例** ✅
eg1不规范写法
```api
info(
foo: foo value
bar:"bar value"
desc:"long long long long
long long text"
)
```
eg2规范写法(推荐)
```api
info(
foo: "foo value"
bar: "bar value"
desc: "long long long long long long text"
)
```
**错误语法示例** ❌
eg1没有key-value内容
```api
info()
```
eg2不包含冒号
```api
info(
foo value
)
```
eg3key-value没有换行
```api
info(foo:"value")
```
eg4没有key
```api
info(
: "value"
)
```
eg5非法的key
```api
info(
12: "value"
)
```
eg6移除旧版本多行语法
```api
info(
foo: >
some text
<
)
```
## type语法块
在api服务中我们需要用到一个结构体(类)来作为请求体,响应体的载体,因此我们需要声明一些结构体来完成这件事情, type语法块由golang的type演变而来当然也保留着一些golang type的特性沿用golang特性有
* 保留了golang内置数据类型`bool`,`int`,`int8`,`int16`,`int32`,`int64`,`uint`,`uint8`,`uint16`,`uint32`,`uint64`,`uintptr`
,`float32`,`float64`,`complex64`,`complex128`,`string`,`byte`,`rune`,
* 兼容golang struct风格声明
* 保留golang关键字
> **[!WARNING]
> * 不支持alias
> * 不支持time.Time数据类型
> * 结构体名称、字段名称、不能为golang关键字
**语法定义**
由于其和golang相似因此不做详细说明具体语法定义请在 [ApiParser.g4](https://github.com/tal-tech/go-zero/blob/master/tools/goctl/api/parser/g4/ApiParser.g4) 中查看typeSpec定义。
**语法说明**
参考golang写法
**正确语法示例** ✅
eg1不规范写法
```api
type Foo struct{
Id int `path:"id"` // ①
Foo int `json:"foo"`
}
type Bar struct{
// 非导出型字段
bar int `form:"bar"`
}
type(
// 非导出型结构体
fooBar struct{
FooBar int
}
)
```
eg2规范写法推荐
```api
type Foo{
Id int `path:"id"`
Foo int `json:"foo"`
}
type Bar{
Bar int `form:"bar"`
}
type(
FooBar{
FooBar int
}
)
```
**错误语法示例** ❌
eg
```api
type Gender int // 不支持
// 非struct token
type Foo structure{
CreateTime time.Time // 不支持time.Time
}
// golang关键字 var
type var{}
type Foo{
// golang关键字 interface
Foo interface
}
type Foo{
foo int
// map key必须要golang内置数据类型
m map[Bar]string
}
```
> [!NOTE] ①
> tag定义和golang中json tag语法一样除了json tag外go-zero还提供了另外一些tag来实现对字段的描述
> 详情见下表。
* tag表
<table>
<tr>
<td>tag key</td> <td>描述</td> <td>提供方</td><td>有效范围 </td> <td>示例 </td>
</tr>
<tr>
<td>json</td> <td>json序列化tag</td> <td>golang</td> <td>request、response</td> <td><code>json:"fooo"</code></td>
</tr>
<tr>
<td>path</td> <td>路由path<code>/foo/:id</code></td> <td>go-zero</td> <td>request</td> <td><code>path:"id"</code></td>
</tr>
<tr>
<td>form</td> <td>标志请求体是一个formPOST方法时或者一个query(GET方法时<code>/search?name=keyword</code>)</td> <td>go-zero</td> <td>request</td> <td><code>form:"name"</code></td>
</tr>
</table>
* tag修饰符
常见参数校验描述
<table>
<tr>
<td>tag key </td> <td>描述 </td> <td>提供方 </td> <td>有效范围 </td> <td>示例 </td>
</tr>
<tr>
<td>optional</td> <td>定义当前字段为可选参数</td> <td>go-zero</td> <td>request</td> <td><code>json:"name,optional"</code></td>
</tr>
<tr>
<td>options</td> <td>定义当前字段的枚举值,多个以竖线|隔开</td> <td>go-zero</td> <td>request</td> <td><code>json:"gender,options=male"</code></td>
</tr>
<tr>
<td>default</td> <td>定义当前字段默认值</td> <td>go-zero</td> <td>request</td> <td><code>json:"gender,default=male"</code></td>
</tr>
<tr>
<td>range</td> <td>定义当前字段数值范围</td> <td>go-zero</td> <td>request</td> <td><code>json:"age,range=[0:120]"</code></td>
</tr>
</table>
> [!TIP]
> tag修饰符需要在tag value后以引文逗号,隔开
## service语法块
service语法块用于定义api服务包含服务名称服务metadata中间件声明路由handler等。
> **[!WARNING]
> * main api和被import的api服务名称必须一致不能出现服务名称歧义。
> * handler名称不能重复
> * 路由(请求方法+请求path名称不能重复
> * 请求体必须声明为普通非指针struct响应体做了一些向前兼容处理详请见下文说明
>
**语法定义**
```antlrv4
serviceSpec: atServer? serviceApi;
atServer: '@server' lp='(' kvLit+ rp=')';
serviceApi: {match(p,"service")}serviceToken=ID serviceName lbrace='{' serviceRoute* rbrace='}';
serviceRoute: atDoc? (atServer|atHandler) route;
atDoc: '@doc' lp='('? ((kvLit+)|STRING) rp=')'?;
atHandler: '@handler' ID;
route: {checkHttpMethod(p)}httpMethod=ID path request=body? returnToken=ID? response=replybody?;
body: lp='(' (ID)? rp=')';
replybody: lp='(' dataType? rp=')';
// kv
kvLit: key=ID {checkKeyValue(p)}value=LINE_VALUE;
serviceName: (ID '-'?)+;
path: (('/' (ID ('-' ID)*))|('/:' (ID ('-' ID)?)))+;
```
**语法说明**
serviceSpec包含了一个可选语法块`atServer`和`serviceApi`语法块其遵循序列模式编写service必须要按照顺序否则会解析出错
atServer 可选语法块定义key-value结构的server metadata'@server'
表示这一个server语法块的开始其可以用于描述serviceApi或者route语法块其用于描述不同语法块时有一些特殊关键key 需要值得注意,见 **atServer关键key描述说明**
serviceApi包含了1到多个`serviceRoute`语法块
serviceRoute按照序列模式包含了`atDoc`,handler和`route`
atDoc可选语法块一个路由的key-value描述其在解析后会传递到spec.Spec结构体如果不关心传递到spec.Spec, 推荐用单行注释替代。
handler是对路由的handler层描述可以通过atServer指定`handler` key来指定handler名称 也可以直接用atHandler语法块来定义handler名称
atHandler'@handler' 固定token后接一个遵循正则`[_a-zA-Z][a-zA-Z_-]*`)的值用于声明一个handler名称
route路由有`httpMethod`、`path`、可选`request`、可选`response`组成,`httpMethod`是必须是小写。
bodyapi请求体语法定义必须要由()包裹的可选的ID值
replyBodyapi响应体语法定义必须由()包裹的struct、~~array(向前兼容处理后续可能会废弃强烈推荐以struct包裹不要直接用array作为响应体)~~
kvLit 同info key-value
serviceName: 可以有多个'-'join的ID值
pathapi请求路径必须以'/'或者'/:'开头,切不能以'/'结尾中间可包含ID或者多个以'-'join的ID字符串
**atServer关键key描述说明**
修饰service时
<table>
<tr>
<td>key</td><td>描述</td><td>示例</td>
</tr>
<tr>
<td>jwt</td><td>声明当前service下所有路由需要jwt鉴权且会自动生成包含jwt逻辑的代码</td><td><code>jwt: Auth</code></td>
</tr>
<tr>
<td>group</td><td>声明当前service或者路由文件分组</td><td><code>group: login</code></td>
</tr>
<tr>
<td>middleware</td><td>声明当前service需要开启中间件</td><td><code>middleware: AuthMiddleware</code></td>
</tr>
</table>
修饰route时
<table>
<tr>
<td>key</td><td>描述</td><td>示例</td>
</tr>
<tr>
<td>handler</td><td>声明一个handler</td><td>-</td>
</tr>
</table>
**正确语法示例** ✅
eg1不规范写法
```api
@server(
jwt: Auth
group: foo
middleware: AuthMiddleware
)
service foo-api{
@doc(
summary: foo
)
@server(
handler: foo
)
// 非导出型body
post /foo/:id (foo) returns (bar)
@doc "bar"
@handler bar
post /bar returns ([]int)// 不推荐数组作为响应体
@handler fooBar
post /foo/bar (Foo) returns // 可以省略'returns'
}
```
eg2规范写法推荐
```api
@server(
jwt: Auth
group: foo
middleware: AuthMiddleware
)
service foo-api{
@doc "foo"
@handler foo
post /foo/:id (Foo) returns (Bar)
}
service foo-api{
@handler ping
get /ping
@doc "foo"
@handler bar
post /bar/:id (Foo)
}
```
**错误语法示例** ❌
```api
// 不支持空的server语法块
@server(
)
// 不支持空的service语法块
service foo-api{
}
service foo-api{
@doc kkkk // 简版doc必须用英文双引号引起来
@handler foo
post /foo
@handler foo // 重复的handler
post /bar
@handler fooBar
post /bar // 重复的路由
// @handler和@doc顺序错误
@handler someHandler
@doc "some doc"
post /some/path
// handler缺失
post /some/path/:id
@handler reqTest
post /foo/req (*Foo) // 不支持除普通结构体外的其他数据类型作为请求体
@handler replyTest
post /foo/reply returns (*Foo) // 不支持除普通结构体、数组(向前兼容,后续考虑废弃)外的其他数据类型作为响应体
}
```
## 隐藏通道
隐藏通道目前主要为空白符号、换行符号以及注释,这里我们只说注释,因为空白符号和换行符号我们目前拿来也无用。
### 单行注释
**语法定义**
```antlrv4
'//' ~[\r\n]*
```
**语法说明**
由语法定义可知道,单行注释必须要以`//`开头,内容为不能包含换行符
**正确语法示例** ✅
```api
// doc
// comment
```
**错误语法示例** ❌
```api
// break
line comments
```
### java风格文档注释
**语法定义**
```antlrv4
'/*' .*? '*/'
```
**语法说明**
由语法定义可知道,单行注释必须要以`/*`开头,`*/`结尾的任意字符。
**正确语法示例** ✅
```api
/**
* java-style doc
*/
```
**错误语法示例** ❌
```api
/*
* java-style doc */
*/
```
## Doc&Comment
如果想获取某一个元素的doc或者comment开发人员需要怎么定义
**Doc**
我们规定上一个语法块非隐藏通道内容的行数line+1到当前语法块第一个元素前的所有注释(当行,或者多行)均为doc 且保留了`//`、`/*`、`*/`原始标记。
**Comment**
我们规定当前语法块最后一个元素所在行开始的一个注释块(当行,或者多行)为comment 且保留了`//`、`/*`、`*/`原始标记。
语法块Doc和Comment的支持情况
<table>
<tr>
<td>语法块</td><td>parent语法块</td><td>Doc</td><td>Comment</td>
</tr>
<tr>
<td>syntaxLit</td><td>api</td><td></td><td></td>
</tr>
<tr>
<td>kvLit</td><td>infoSpec</td><td></td><td></td>
</tr>
<tr>
<td>importLit</td><td>importSpec</td><td></td><td></td>
</tr>
<tr>
<td>typeLit</td><td>api</td><td></td><td></td>
</tr>
<tr>
<td>typeLit</td><td>typeBlock</td><td></td><td></td>
</tr>
<tr>
<td>field</td><td>typeLit</td><td></td><td></td>
</tr>
<tr>
<td>key-value</td><td>atServer</td><td></td><td></td>
</tr>
<tr>
<td>atHandler</td><td>serviceRoute</td><td></td><td></td>
</tr>
<tr>
<td>route</td><td>serviceRoute</td><td></td><td></td>
</tr>
</table>
以下为对应语法块解析后细带doc和comment的写法
```api
// syntaxLit doc
syntax = "v1" // syntaxLit commnet
info(
// kvLit doc
author: songmeizi // kvLit comment
)
// typeLit doc
type Foo {}
type(
// typeLit doc
Bar{}
FooBar{
// filed doc
Name int // filed comment
}
)
@server(
/**
* kvLit doc
* 开启jwt鉴权
*/
jwt: Auth /**kvLit comment*/
)
service foo-api{
// atHandler doc
@handler foo //atHandler comment
/*
* route doc
* post请求
* path为 /foo
* 请求体Foo
* 响应体Foo
*/
post /foo (Foo) returns (Foo) // route comment
}
```

77
go-zero.dev/cn/bloom.md Normal file
View File

@ -0,0 +1,77 @@
# bloom
go-zero微服务框架中提供了许多开箱即用的工具好的工具不仅能提升服务的性能而且还能提升代码的鲁棒性避免出错实现代码风格的统一方便他人阅读等等本系列文章将分别介绍go-zero框架中工具的使用及其实现原理
## 布隆过滤器[bloom](https://github.com/zeromicro/go-zero/blob/master/core/bloom/bloom.go)
在做服务器开发的时候,相信大家有听过布隆过滤器,可以判断某元素在不在集合里面,因为存在一定的误判和删除复杂问题,一般的使用场景是:防止缓存击穿(防止恶意攻击)、 垃圾邮箱过滤、cache digests 、模型检测器等、判断是否存在某行数据,用以减少对磁盘访问,提高服务的访问性能。     go-zero 提供的简单的缓存封装 bloom.bloom简单使用方式如下
```go
// 初始化 redisBitSet
store := redis.NewRedis("redis 地址", redis.NodeType)
// 声明一个bitSet, key="test_key"名且bits是1024位
bitSet := newRedisBitSet(store, "test_key", 1024)
// 判断第0位bit存不存在
isSetBefore, err := bitSet.check([]uint{0})
// 对第512位设置为1
err = bitSet.set([]uint{512})
// 3600秒后过期
err = bitSet.expire(3600)
// 删除该bitSet
err = bitSet.del()
```
bloom 简单介绍了最基本的redis bitset 的使用。下面是真正的bloom实现。
对元素hash 定位
```go
// 对元素进行hash 14次(const maps=14),每次都在元素后追加byte(0-13),然后进行hash.
// 将locations[0-13] 进行取模,最终返回locations.
func (f *BloomFilter) getLocations(data []byte) []uint {
locations := make([]uint, maps)
for i := uint(0); i < maps; i++ {
hashValue := hash.Hash(append(data, byte(i)))
locations[i] = uint(hashValue % uint64(f.bits))
}
return locations
}
```
向bloom里面add 元素
```go
// 我们可以发现 add方法使用了getLocations和bitSet的set方法。
// 我们将元素进行hash成长度14的uint切片,然后进行set操作存到redis的bitSet里面。
func (f *BloomFilter) Add(data []byte) error {
locations := f.getLocations(data)
err := f.bitSet.set(locations)
if err != nil {
return err
}
return nil
}
```
检查bloom里面是否有某元素
```go
// 我们可以发现 Exists方法使用了getLocations和bitSet的check方法
// 我们将元素进行hash成长度14的uint切片,然后进行bitSet的check验证,存在返回true,不存在或者check失败返回false
func (f *BloomFilter) Exists(data []byte) (bool, error) {
locations := f.getLocations(data)
isSet, err := f.bitSet.check(locations)
if err != nil {
return false, err
}
if !isSet {
return false, nil
}
return true, nil
}
```
本节主要介绍了go-zero框架中的 core.bloom 工具,在实际的项目中非常实用。用好工具对于提升服务性能和开发效率都有很大的帮助,希望本篇文章能给大家带来一些收获。

View File

@ -2,7 +2,7 @@
在微服务中服务间依赖非常常见,比如评论服务依赖审核服务而审核服务又依赖反垃圾服务,当评论服务调用审核服务时,审核服务又调用反垃圾服务,而这时反垃圾服务超时了,由于审核服务依赖反垃圾服务,反垃圾服务超时导致审核服务逻辑一直等待,而这个时候评论服务又在一直调用审核服务,审核服务就有可能因为堆积了大量请求而导致服务宕机
<img src="./images/call_chain.png" alt="call_chain" style="zoom:60%;" />
![call_chain](./resource/call_chain.png)
由此可见,在整个调用链中,中间的某一个环节出现异常就会引起上游调用服务出现一些列的问题,甚至导致整个调用链的服务都宕机,这是非常可怕的。因此一个服务作为调用方调用另一个服务时,为了防止被调用服务出现问题进而导致调用服务出现问题,所以调用服务需要进行自我保护,而保护的常用手段就是***熔断***
@ -16,7 +16,7 @@
- 打开(Open):在该状态下,发起请求时会立即返回错误,一般会启动一个超时计时器,当计时器超时后,状态切换到半打开状态,也可以设置一个定时器,定期的探测服务是否恢复
- 半打开(Half-Open):在该状态下,允许应用程序一定数量的请求发往被调用服务,如果这些调用正常,那么可以认为被调用服务已经恢复正常,此时熔断器切换到关闭状态,同时需要重置计数。如果这部分仍有调用失败的情况,则认为被调用方仍然没有恢复,熔断器会切换到关闭状态,然后重置计数器,半打开状态能够有效防止正在恢复中的服务被突然大量请求再次打垮
<img src="./images/breaker_state.png" alt="breaker_state" style="zoom:50%;" />
![breaker_state](./resource/breaker_state.png)
服务治理中引入熔断机制,使得系统更加稳定和有弹性,在系统从错误中恢复的时候提供稳定性,并且减少了错误对系统性能的影响,可以快速拒绝可能导致错误的服务调用,而不需要等待真正的错误返回
@ -26,7 +26,7 @@
我们知道熔断器主要是用来保护调用端调用端在发起请求的时候需要先经过熔断器而客户端拦截器正好兼具了这个这个功能所以在zRPC框架内熔断器是实现在客户端拦截器内拦截器的原理如下图
<img src="./images/interceptor.png" alt="interceptor" style="zoom:50%;" />
![interceptor](./resource/interceptor.png)
对应的代码为:
@ -52,7 +52,7 @@ zRPC中熔断器的实现参考了[Google Sre过载保护算法](https://landing
在正常情况下,这两个值是相等的,随着被调用方服务出现异常开始拒绝请求,请求接受数量(accepts)的值开始逐渐小于请求数量(requests)这个时候调用方可以继续发送请求直到requests = K * accepts一旦超过这个限制熔断器就回打开新的请求会在本地以一定的概率被抛弃直接返回错误概率的计算公式如下
<img src="./images/client_rejection2.png" alt="client_rejection2" style="zoom:30%;" />
![client_rejection2](./resource/client_rejection2.png)
通过修改算法中的K(倍值),可以调节熔断器的敏感度,当降低该倍值会使自适应熔断算法更敏感,当增加该倍值会使得自适应熔断算法降低敏感度,举例来说,假设将调用方的请求上限从 requests = 2 * acceptst 调整为 requests = 1.1 * accepts 那么就意味着调用方每十个请求之中就有一个请求会触发熔断

View File

@ -0,0 +1,125 @@
# go-zero缓存设计之业务层缓存
在上一篇[go-zero缓存设计之持久层缓存](redis-cache.md)介绍了db层缓存回顾一下db层缓存主要设计可以总结为
* 缓存只删除不更新
* 行记录始终只存储一份,即主键对应行记录
* 唯一索引仅缓存主键值不直接缓存行记录参考mysql索引思想
* 防缓存穿透设计,默认一分钟
* 不缓存多行记录
## 前言
在大型业务系统中,通过对持久层添加缓存,对于大多数单行记录查询,相信缓存能够帮持久层减轻很大的访问压力,但在实际业务中,数据读取不仅仅只是单行记录,
面对大量多行记录的查询,这对持久层也会造成不小的访问压力,除此之外,像秒杀系统、选课系统这种高并发的场景,单纯靠持久层的缓存是不现实的,本节我们来 介绍go-zero实践中的缓存设计——biz缓存。
## 适用场景举例
* 选课系统
* 内容社交系统
* 秒杀 ...
像这些系统,我们可以在业务层再增加一层缓存来存储系统中的关键信息,如选课系统中学生选课信息,课程剩余名额;内容社交系统中某一段时间之间的内容信息等。
接下来,我们一内容社交系统来进行举例说明。
在内容社交系统中,我们一般是先查询一批内容列表,然后点击某条内容查看详情,
在没有添加biz缓存前内容信息的查询流程图应该为
![redis-cache-05](./resource/redis-cache-05.png)
从图以及上一篇文章[go-zero缓存设计之持久层缓存](redis-cache.md)中我们可以知道,内容列表的获取是没办法依赖缓存的,
如果我们在业务层添加一层缓存用来存储列表中的关键信息甚至完整信息那么多行记录的访问不在是一个问题这就是biz redis要做的事情。 接下来我们来看一下设计方案,假设内容系统中单行记录包含以下字段
|字段名称|字段类型|备注|
|---|---|---|
|id|string|内容id|
|title|string|标题|
|content|string|详细内容|
|createTime|time.Time|创建时间|
我们的目标是获取一批内容列表而尽量避免内容列表走db造成访问压力首先我们采用redis的sort set数据结构来存储根需要存储的字段信息量有两种redis存储方案
* 缓存局部信息
![biz-redis-02](./resource/biz-redis-02.svg)
对其关键字段信息id等按照一定规则压缩并存储score我们用`createTime`毫秒值时间值相等这里不讨论这种存储方案的好处是节约redis存储空间
那另一方面,缺点就是需要对列表详细内容进行二次回查(但这次回查是会利用到持久层的行记录缓存的)
* 缓存完整信息
![biz-redis-01](./resource/biz-redis-01.svg)
对发布的所有内容按照一定规则压缩后均进行存储同样score我们还是用`createTime`毫秒值这种存储方案的好处是业务的增、删、查、改均走reids而db层这时候
就可以不用考虑行记录缓存了,持久层仅提供数据备份和恢复使用,从另一方面来看,其缺点也很明显,需要的存储空间、配置要求更高,费用也会随之增大。
示例代码:
```golang
type Content struct {
Id string `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
CreateTime time.Time `json:"create_time"`
}
const bizContentCacheKey = `biz#content#cache`
// AddContent 提供内容存储
func AddContent(r redis.Redis, c *Content) error {
v := compress(c)
_, err := r.Zadd(bizContentCacheKey, c.CreateTime.UnixNano()/1e6, v)
return err
}
// DelContent 提供内容删除
func DelContent(r redis.Redis, c *Content) error {
v := compress(c)
_, err := r.Zrem(bizContentCacheKey, v)
return err
}
// 内容压缩
func compress(c *Content) string {
// todo: do it yourself
var ret string
return ret
}
// 内容解压
func unCompress(v string) *Content {
// todo: do it yourself
var ret Content
return &ret
}
// ListByRangeTime提供根据时间段进行数据查询
func ListByRangeTime(r redis.Redis, start, end time.Time) ([]*Content, error) {
kvs, err := r.ZrangebyscoreWithScores(bizContentCacheKey, start.UnixNano()/1e6, end.UnixNano()/1e6)
if err != nil {
return nil, err
}
var list []*Content
for _, kv := range kvs {
data:=unCompress(kv.Key)
list = append(list, data)
}
return list, nil
}
```
在以上例子中redis是没有设置过期时间的我们将增、删、改、查操作均同步到redis我们认为内容社交系统的列表访问请求是比较高的情况下才做这样的方案设计
除此之外,还有一些数据访问,没有想内容设计系统这么频繁的访问, 可能是某一时间段内访问量突如其来的增加,之后可能很长一段时间才会再访问一次,以此间隔,
或者说不会再访问了面对这种场景如果我又该如何考虑缓存的设计呢在go-zero内容实践中有两种方案可以解决这种问题
* 增加内存缓存通过内存缓存来存储当前可能突发访问量比较大的数据常用的存储方案采用map数据结构来存储map数据存储实现比较简单但缓存过期处理则需要增加
定时器来出来另一宗方案是通过go-zero库中的 [Cache](https://github.com/tal-tech/go-zero/blob/master/core/collection/cache.go) ,其是专门
用于内存管理.
* 采用biz redis,并设置合理的过期时间
# 总结
以上两个场景可以包含大部分的多行记录缓存对于多行记录查询量不大的场景暂时没必要直接把biz redis放进去可以先尝试让db来承担开发人员可以根据持久层监控及服务
监控来衡量时候需要引入biz。

View File

@ -0,0 +1,124 @@
# 业务编码
前面一节我们已经根据初步需求编写了user.api来描述user服务对外提供哪些服务访问在本节我们接着前面的步伐
通过业务编码来讲述go-zero怎么在实际业务中使用。
## 添加Mysql配置
```shell
$ vim service/user/cmd/api/internal/config/config.go
```
```go
package config
import "github.com/tal-tech/go-zero/rest"
type Config struct {
rest.RestConf
Mysql struct{
DataSource string
}
CacheRedis cache.CacheConf
}
```
## 完善yaml配置
```shell
$ vim service/user/cmd/api/etc/user-api.yaml
```
```yaml
Name: user-api
Host: 0.0.0.0
Port: 8888
Mysql:
DataSource: $user:$password@tcp($url)/$db?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai
CacheRedis:
- Host: $host
Pass: $pass
Type: node
```
> [!TIP]
> $user: mysql数据库user
>
> $password: mysql数据库密码
>
> $url: mysql数据库连接地址
>
> $db: mysql数据库db名称即user表所在database
>
> $host: redis连接地址 格式ip:port如:127.0.0.1:6379
>
> $pass: redis密码
>
> 更多配置信息,请参考[api配置介绍](api-config.md)
## 完善服务依赖
```shell
$ vim service/user/cmd/api/internal/svc/servicecontext.go
```
```go
type ServiceContext struct {
Config config.Config
UserModel model.UserModel
}
func NewServiceContext(c config.Config) *ServiceContext {
conn:=sqlx.NewMysql(c.Mysql.DataSource)
return &ServiceContext{
Config: c,
UserModel: model.NewUserModel(conn,c.CacheRedis),
}
}
```
## 填充登录逻辑
```shell
$ vim service/user/cmd/api/internal/logic/loginlogic.go
```
```go
func (l *LoginLogic) Login(req types.LoginReq) (*types.LoginReply, error) {
if len(strings.TrimSpace(req.Username)) == 0 || len(strings.TrimSpace(req.Password)) == 0 {
return nil, errors.New("参数错误")
}
userInfo, err := l.svcCtx.UserModel.FindOneByNumber(req.Username)
switch err {
case nil:
case model.ErrNotFound:
return nil, errors.New("用户名不存在")
default:
return nil, err
}
if userInfo.Password != req.Password {
return nil, errors.New("用户密码不正确")
}
// ---start---
now := time.Now().Unix()
accessExpire := l.svcCtx.Config.Auth.AccessExpire
jwtToken, err := l.getJwtToken(l.svcCtx.Config.Auth.AccessSecret, now, l.svcCtx.Config.Auth.AccessExpire, userInfo.Id)
if err != nil {
return nil, err
}
// ---end---
return &types.LoginReply{
Id: userInfo.Id,
Name: userInfo.Name,
Gender: userInfo.Gender,
AccessToken: jwtToken,
AccessExpire: now + accessExpire,
RefreshAfter: now + accessExpire/2,
}, nil
}
```
> [!TIP]
> 上述代码中 [start]-[end]的代码实现见[jwt鉴权](jwt.md)章节
# 猜你想看
* [api语法](api-grammar.md)
* [goctl api命令](goctl-api.md)
* [api目录结构介绍](api-dir.md)
* [jwt鉴权](jwt.md)
* [api配置介绍](api-config.md)

View File

@ -0,0 +1,59 @@
# 业务开发
本章节我们用一个简单的示例去演示一下go-zero中的一些基本功能。本节将包含以下小节
* [目录拆分](service-design.md)
* [model生成](model-gen.md)
* [api文件编写](api-coding.md)
* [业务编码](business-coding.md)
* [jwt鉴权](jwt.md)
* [中间件使用](middleware.md)
* [rpc服务编写与调用](rpc-call.md)
* [错误处理](error-handle.md)
## 演示工程下载
在正式进入后续文档叙述前,可以先留意一下这里的源码,后续我们会基于这份源码进行功能的递进式演示,
而不是完全从0开始如果你从[快速入门](quick-start.md)章节过来,这份源码结构对你来说不是问题。
点击<a href="https://zeromicro.github.io/go-zero-pages/resource/book.zip">这里下载</a>演示工程基础源码
## 演示工程说明
### 场景
程序员小明需要借阅一本《西游记》,在没有线上图书管理系统的时候,他每天都要去图书馆前台咨询图书馆管理员,
* 小明:你好,请问今天《西游记》的图书还有吗?
* 管理员:没有了,明天再来看看吧。
过了一天,小明又来到图书馆,问:
* 小明:你好,请问今天《西游记》的图书还有吗?
* 管理员:没有了,你过两天再来看看吧。
就这样经过多次反复,小明也是徒劳无功,浪费大量时间在来回的路上,于是终于忍受不了落后的图书管理系统,
他决定自己亲手做一个图书查阅系统。
### 预期实现目标
* 用户登录
依靠现有学生系统数据进行登录
* 图书检索
根据图书关键字搜索图书,查询图书剩余数量。
### 系统分析
#### 服务拆分
* user
* api 提供用户登录协议
* rpc 供search服务访问用户数据
* search
* api 提供图书查询协议
> [!TIP]
> 这个微小的图书借阅查询系统虽然小从实际来讲不太符合业务场景但是仅上面两个功能已经满足我们对go-zero api/rpc的场景演示了
> 后续为了满足更丰富的go-zero功能演示会在文档中进行业务插入即相关功能描述。这里仅用一个场景进行引入。
>
> 注意user中的sql语句请自行创建到db中去更多准备工作见[准备工作](prepare.md)
>
> 添加一些预设的用户数据到数据库,便于后面使用,为了篇幅,演示工程不对插入数据这种操作做详细演示。
# 参考预设数据
```sql
INSERT INTO `user` (number,name,password,gender)values ('666','小明','123456','男');
```

57
go-zero.dev/cn/ci-cd.md Normal file
View File

@ -0,0 +1,57 @@
# CI/CD
> 在软件工程中CI/CD或CICD通常指的是持续集成和持续交付或持续部署的组合实践。
> ——引自[维基百科](https://zh.wikipedia.org/wiki/CI/CD)
![cd-cd](./resource/ci-cd.png)
## CI可以做什么
> 现代应用开发的目标是让多位开发人员同时处理同一应用的不同功能。但是如果企业安排在一天内将所有分支源代码合并在一起称为“合并日”最终可能造成工作繁琐、耗时而且需要手动完成。这是因为当一位独立工作的开发人员对应用进行更改时有可能会与其他开发人员同时进行的更改发生冲突。如果每个开发人员都自定义自己的本地集成开发环境IDE而不是让团队就一个基于云的 IDE 达成一致,那么就会让问题更加雪上加霜。
> 持续集成CI可以帮助开发人员更加频繁地有时甚至每天将代码更改合并到共享分支或“主干”中。一旦开发人员对应用所做的更改被合并系统就会通过自动构建应用并运行不同级别的自动化测试通常是单元测试和集成测试来验证这些更改确保这些更改没有对应用造成破坏。这意味着测试内容涵盖了从类和函数到构成整个应用的不同模块。如果自动化测试发现新代码和现有代码之间存在冲突CI 可以更加轻松地快速修复这些错误。
> ——引自[《CI/CD是什么如何理解持续集成、持续交付和持续部署》](https://www.redhat.com/zh/topics/devops/what-is-ci-cd)
从概念上来看CI/CD包含部署过程我们这里将部署(CD)单独放在一节[服务部署](service-deployment.md)
本节就以gitlab来做简单的CIRun Unit Test演示。
## gitlab CI
Gitlab CI/CD是Gitlab内置的软件开发工具提供
* 持续集成(CI)
* 持续交付(CD)
* 持续部署(CD)
## 准备工作
* gitlab安装
* git安装
* gitlab runner安装
## 开启gitlab CI
* 上传代码
* 在gitlab新建一个仓库`go-zero-demo`
* 将本地代码上传到`go-zero-demo`仓库
* 在项目根目录下创建`.gitlab-ci.yaml`文件通过此文件可以创建一个pipeline其会在代码仓库中有内容变更时运行pipeline由一个或多个按照顺序运行
每个阶段可以包含一个或者多个并行运行的job。
* 添加CI内容(仅供参考)
```yaml
stages:
- analysis
analysis:
stage: analysis
image: golang
script:
- go version && go env
- go test -short $(go list ./...) | grep -v "no test"
```
> [!TIP]
> 以上CI为简单的演示详细的gitlab CI请参考gitlab官方文档进行更丰富的CI集成。
# 参考文档
* [CI/CD 维基百科](https://zh.wikipedia.org/wiki/CI/CD)
* [CI/CD是什么如何理解持续集成、持续交付和持续部署](https://www.redhat.com/zh/topics/devops/what-is-ci-cd)
* [Gitlab CI](https://docs.gitlab.com/ee/ci/)

View File

@ -0,0 +1,43 @@
# 编码规范
## import
* 单行import不建议用圆括号包裹
* 按照`官方包`NEW LINE`当前工程包`NEW LINE`第三方依赖包`顺序引入
```go
import (
"context"
"string"
"greet/user/internal/config"
"google.golang.org/grpc"
)
```
## 函数返回
* 对象避免非指针返回
* 遵循有正常值返回则一定无error有error则一定无正常值返回的原则
## 错误处理
* 有error必须处理如果不能处理就必须抛出。
* 避免下划线(_)接收error
## 函数体编码
* 建议一个block结束空一行如if、for等
```go
func main (){
if x==1{
// do something
}
fmt.println("xxx")
}
```
* return前空一行
```go
func getUser(id string)(string,error){
....
return "xx",nil
}
```

View File

@ -2,7 +2,7 @@
go-zero微服务框架中提供了许多开箱即用的工具好的工具不仅能提升服务的性能而且还能提升代码的鲁棒性避免出错实现代码风格的统一方便他人阅读等等本系列文章将分别介绍go-zero框架中工具的使用及其实现原理
## 进程内缓存工具[collection.Cache](https://github.com/tal-tech/go-zero/tree/master/core/collection/cache.go)
## 进程内缓存工具[collection.Cache](https://github.com/zeromicro/go-zero/tree/master/core/collection/cache.go)
在做服务器开发的时候相信都会遇到使用缓存的情况go-zero 提供的简单的缓存封装 **collection.Cache**,简单使用方式如下
@ -38,7 +38,7 @@ cache 实现的建的功能包括
* 缓存击穿
实现原理:
Cache 自动失效,是采用 TimingWheel(https://github.com/tal-tech/go-zero/blob/master/core/collection/timingwheel.go) 进行管理的
Cache 自动失效,是采用 TimingWheel(https://github.com/tal-tech/zeromicro/blob/master/core/collection/timingwheel.go) 进行管理的
``` go
timingWheel, err := NewTimingWheel(time.Second, slots, func(k, v interface{}) {
@ -79,7 +79,7 @@ Cache 的命中率统计,是在代码中实现 cacheStat,在缓存命中丢失
cache(proc) - qpm: 2, hit_ratio: 50.0%, elements: 0, hit: 1, miss: 1
```
缓存击穿包含是使用 syncx.SharedCalls(https://github.com/tal-tech/go-zero/blob/master/core/syncx/sharedcalls.go) 进行实现的,就是将同时请求同一个 key 的请求, 关于 sharedcalls 后续会继续补充。 相关具体实现是在:
缓存击穿包含是使用 syncx.SharedCalls(https://github.com/tal-tech/zeromicro/blob/master/core/syncx/sharedcalls.go) 进行实现的,就是将同时请求同一个 key 的请求, 关于 sharedcalls 后续会继续补充。 相关具体实现是在:
```go
func (c *Cache) Take(key string, fetch func() (interface{}, error)) (interface{}, error) {

View File

@ -0,0 +1,41 @@
# 概念介绍
## go-zero
晓黑板golang开源项目集各种工程实践于一身的web和rpc框架。
## goctl
一个旨在为开发人员提高工程效率、降低出错率的辅助工具。
## goctl插件
指以goctl为中心的周边二进制资源能够满足一些个性化的代码生成需求如路由合并插件`goctl-go-compact`插件,
生成swagger文档的`goctl-swagger`插件生成php调用端的`goctl-php`插件等。
## intellij/vscode插件
在intellij系列产品上配合goctl开发的插件其将goctl命令行操作使用UI进行替代。
## api文件
api文件是指用于定义和描述api服务的文本文件其以.api后缀结尾包含api语法描述内容。
## goctl环境
goctl环境是使用goctl前的准备环境包含
* golang环境
* protoc
* protoc-gen-go插件
* go module | gopath
## go-zero-demo
go-zero-demo里面包含了文档中所有源码的一个大仓库后续我们在编写演示demo时我们均在此项目下创建子项目
因此我们需要提前创建一个大仓库`go-zero-demo`我这里把这个仓库放在home目录下。
```shell
$ cd ~
$ mkdir go-zero-demo&&cd go-zero-demo
$ go mod init go-zero-demo
```
# 参考文档
* [go-zero](README.md)
* [Goctl](goctl.md)
* [插件中心](plugin-center.md)
* [工具中心](tool-center.md)
* [api语法](api-grammar.md)

View File

@ -0,0 +1,4 @@
# 配置介绍
在正式使用go-zero之前让我们先来了解一下go-zero中不同服务类型的配置定义看看配置中每个字段分别有什么作用本节将包含以下小节
* [api配置](api-config.md)
* [rpc配置](rpc-config.md)

1053
go-zero.dev/cn/datacenter.md Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,25 @@
# 开发流程
这里的开发流程和我们实际业务开发流程不是一个概念这里的定义局限于go-zero的使用即代码层面的开发细节。
## 开发流程
* goctl环境准备[1]
* 数据库设计
* 业务开发
* 新建工程
* 创建服务目录
* 创建服务类型api/rpc/rmq/job/script
* 编写api、proto文件
* 代码生成
* 生成数据库访问层代码model
* 配置configyaml变更
* 资源依赖填充ServiceContext
* 添加中间件
* 业务代码填充
* 错误处理
> [!TIP]
> [1] [goctl环境](concept-introduction.md)
## 开发工具
* Visual Studio Code
* Goland(推荐)

View File

@ -0,0 +1,25 @@
# 开发规范
在实际业务开发中,除了要提高业务开发效率,缩短业务开发周期,保证线上业务高性能,高可用的指标外,好的编程习惯也是一个开发人员基本素养之一,在本章节,
我们将介绍一下go-zero中的编码规范本章节为可选章节内容仅供交流与参考本章节将从以下小节进行说明
* [命名规范](naming-spec.md)
* [路由规范](route-naming-spec.md)
* [编码规范](coding-spec.md)
## 开发三原则
### Clarity清晰
作者引用了`Hal Abelson and Gerald Sussman`的一句话:
> Programs must be written for people to read, and only incidentally for machines to execute
程序是什么程序必须是为了开发人员阅读而编写的只是偶尔给机器去执行99%的时间程序代码面向的是开发人员而只有1%的时间可能是机器在执行这里比例不是重点从中我们可以看出清晰的代码是多么的重要因为所有程序不仅是Go语言都是由开发人员编写供其他人阅读和维护。
### Simplicity简单
> Simplicity is prerequisite for reliability
`Edsger W. Dijkstra`认为:可靠的前提条件就是简单,我们在实际开发中都遇到过,这段代码在写什么,想要完成什么事情,开发人员不理解这段代码,因此也不知道如何去维护,这就带来了复杂性,程序越是复杂就越难维护,越难维护就会是程序变得越来越复杂,因此,遇到程序变复杂时首先应该想到的是——重构,重构会重新设计程序,让程序变得简单。
### Productivity生产力
在go-zero团队中一直在强调这个话题开发人员成产力的多少并不是你写了多少行代码完成了多少个模块开发而是我们需要利用各种有效的途径来利用有限的时间完成开发效率最大化而Goctl的诞生正是为了提高生产力
因此这个开发原则我是非常认同的。

View File

@ -0,0 +1,55 @@
# 文档贡献
## 怎么贡献文档?
点击顶部"编辑此页"按钮即可进入源码仓库对应的文件开发人员将修改添加的文档通过pr形式提交
我们收到pr后会进行文档审核一旦审核通过即可更新文档。
![doc-edit](./resource/doc-edit.png)
## 可以贡献哪些文档?
* 文档编写错误
* 文档不规范、不完整
* go-zero应用实践、心得
* 组件中心
## 文档pr通过后文档多久会更新
在pr接受后github action会自动build gitbook并发布因此在github action成功后1-2分钟即可查看更新后的文档。
## 文档贡献注意事项
* 纠错、完善源文档可以直接编写原来的md文件
* 新增组件文档需要保证文档排版、易读,且组件文档需要放在[组件中心](extended-reading.md)子目录中
* go-zero应用实践分享可以直接放在[开发实践](practise.md)子目录下
## 目录结构规范
* 目录结构不宜过深最好不要超过3层
* 组件文档需要在归属到[组件中心](extended-reading.md),如
```markdown
* [开发实践](practise.md)
* [logx](logx.md)
* [bloom](bloom.md)
* [executors](executors.md)
* 你的文档目录名称
```
* 应用实践需要归属到[开发实践](practise.md),如
```markdown
* [开发实践](practise.md)
* [我是如何用go-zero 实现一个中台系统](datacenter.md)
* [流数据处理利器](stream.md)
* [10月3日线上交流问题汇总](online-exchange.md
* 你的文档目录名称
```
## 开发实践文档模板
```markdown
# 标题
> 作者:填入作者名称
>
> 原文连接: 原文连接
some markdown content
```
# 猜你想看
* [怎么参与贡献](join-us.md)
* [Github Pull request](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/proposing-changes-to-your-work-with-pull-requests)

View File

@ -0,0 +1,175 @@
# 错误处理
错误的处理是一个服务必不可缺的环节。在平时的业务开发中我们可以认为http状态码不为`2xx`系列的都可以认为是http请求错误
并伴随响应的错误信息但这些错误信息都是以plain text形式返回的。除此之外我在业务中还会定义一些业务性错误常用做法都是通过
`code`、`msg` 两个字段来进行业务处理结果描述并且希望能够以json响应体来进行响应。
## 业务错误响应格式
* 业务处理正常
```json
{
"code": 0,
"msg": "successful",
"data": {
....
}
}
```
* 业务处理异常
```json
{
"code": 10001,
"msg": "参数错误"
}
```
## user api之login
在之前我们在登录逻辑中处理用户名不存在时直接返回来一个error。我们来登录并传递一个不存在的用户名看看效果。
```shell
curl -X POST \
http://127.0.0.1:8888/user/login \
-H 'content-type: application/json' \
-d '{
"username":"1",
"password":"123456"
}'
```
```text
HTTP/1.1 400 Bad Request
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
Date: Tue, 09 Feb 2021 06:38:42 GMT
Content-Length: 19
用户名不存在
```
接下来我们将其以json格式进行返回
## 自定义错误
* 首先在common中添加一个`baseerror.go`文件,并填入代码
```shell
$ cd common
$ mkdir errorx&&cd errorx
$ vim baseerror.go
```
```goalng
package errorx
const defaultCode = 1001
type CodeError struct {
Code int `json:"code"`
Msg string `json:"msg"`
}
type CodeErrorResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
}
func NewCodeError(code int, msg string) error {
return &CodeError{Code: code, Msg: msg}
}
func NewDefaultError(msg string) error {
return NewCodeError(defaultCode, msg)
}
func (e *CodeError) Error() string {
return e.Msg
}
func (e *CodeError) Data() *CodeErrorResponse {
return &CodeErrorResponse{
Code: e.Code,
Msg: e.Msg,
}
}
```
* 将登录逻辑中错误用CodeError自定义错误替换
```go
if len(strings.TrimSpace(req.Username)) == 0 || len(strings.TrimSpace(req.Password)) == 0 {
return nil, errorx.NewDefaultError("参数错误")
}
userInfo, err := l.svcCtx.UserModel.FindOneByNumber(req.Username)
switch err {
case nil:
case model.ErrNotFound:
return nil, errorx.NewDefaultError("用户名不存在")
default:
return nil, err
}
if userInfo.Password != req.Password {
return nil, errorx.NewDefaultError("用户密码不正确")
}
now := time.Now().Unix()
accessExpire := l.svcCtx.Config.Auth.AccessExpire
jwtToken, err := l.getJwtToken(l.svcCtx.Config.Auth.AccessSecret, now, l.svcCtx.Config.Auth.AccessExpire, userInfo.Id)
if err != nil {
return nil, err
}
return &types.LoginReply{
Id: userInfo.Id,
Name: userInfo.Name,
Gender: userInfo.Gender,
AccessToken: jwtToken,
AccessExpire: now + accessExpire,
RefreshAfter: now + accessExpire/2,
}, nil
```
* 开启自定义错误
```shell
$ vim service/user/cmd/api/user.go
```
```go
func main() {
flag.Parse()
var c config.Config
conf.MustLoad(*configFile, &c)
ctx := svc.NewServiceContext(c)
server := rest.MustNewServer(c.RestConf)
defer server.Stop()
handler.RegisterHandlers(server, ctx)
// 自定义错误
httpx.SetErrorHandler(func(err error) (int, interface{}) {
switch e := err.(type) {
case *errorx.CodeError:
return http.StatusOK, e.Data()
default:
return http.StatusInternalServerError, nil
}
})
fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port)
server.Start()
}
```
* 重启服务验证
```shell
$ curl -i -X POST \
http://127.0.0.1:8888/user/login \
-H 'content-type: application/json' \
-d '{
"username":"1",
"password":"123456"
}'
```
```text
HTTP/1.1 200 OK
Content-Type: application/json
Date: Tue, 09 Feb 2021 06:47:29 GMT
Content-Length: 40
{"code":1001,"msg":"用户名不存在"}
```

46
go-zero.dev/cn/error.md Normal file
View File

@ -0,0 +1,46 @@
# 常见错误处理
## Windows上报错
```text
A required privilege is not held by the client.
```
解决方法:"以管理员身份运行" goctl 即可。
## grpc引起错误
* 错误一
```text
protoc-gen-go: unable to determine Go import path for "greet.proto"
Please specify either:
• a "go_package" option in the .proto source file, or
• a "M" argument on the command line.
See https://developers.google.com/protocol-buffers/docs/reference/go-generated#package for more information.
--go_out: protoc-gen-go: Plugin failed with status code 1.
```
解决方法:
```text
go get -u github.com/golang/protobuf/protoc-gen-go@v1.3.2
```
## protoc-gen-go安装失败
```text
go get github.com/golang/protobuf/protoc-gen-go: module github.com/golang/protobuf/protoc-gen-go: Get "https://proxy.golang.org/github.com/golang/protobuf/protoc-gen-go/@v/list": dial tcp 216.58.200.49:443: i/o timeout
```
请确认`GOPROXY`已经设置,GOPROXY设置见[go module配置](gomod-config.md)
## api服务启动失败
```text
error: config file etc/user-api.yaml, error: type mismatch for field xx
```
请确认`user-api.yaml`配置文件中配置项是否已经配置如果有值检查一下yaml配置文件是否符合yaml格式。
## goctl找不到
```
command not found: goctl
```
请确保goctl已经安装或者goctl是否已经添加到环境变量

325
go-zero.dev/cn/executors.md Normal file
View File

@ -0,0 +1,325 @@
# executors
`go-zero` 中,`executors` 充当任务池,做多任务缓冲,适用于做批量处理的任务。如:`clickhouse` 大批量 `insert``sql batch insert`。同时也可以在 `go-queue` 中看到 `executors` 【在 `queue` 里面使用的是 `ChunkExecutor` ,限定任务提交字节大小】。
所以当你存在以下需求,都可以使用这个组件:
- 批量提交任务
- 缓冲一部分任务,惰性提交
- 延迟任务提交
具体解释之前,先给一个大致的概览图:
![c42c34e8d33d48ec8a63e56feeae882a](./resource/c42c34e8d33d48ec8a63e56feeae882a.png)
## 接口设计
`executors` 包下,有如下几个 `executor`
| Name | Margin value |
| --- | --- |
| `bulkexecutor` | 达到 `maxTasks` 【最大任务数】 提交 |
| `chunkexecutor` | 达到 `maxChunkSize`【最大字节数】提交 |
| `periodicalexecutor` | `basic executor` |
| `delayexecutor` | 延迟执行传入的 `fn()` |
| `lessexecutor` | |
你会看到除了有特殊功能的 `delay``less` ,其余 3 个都是 `executor` + `container` 的组合设计:
```go
func NewBulkExecutor(execute Execute, opts ...BulkOption) *BulkExecutor {
// 选项模式:在 go-zero 中多处出现。在多配置下,比较好的设计思路
// https://halls-of-valhalla.org/beta/articles/functional-options-pattern-in-go,54/
options := newBulkOptions()
for _, opt := range opts {
opt(&options)
}
// 1. task container: [execute 真正做执行的函数] [maxTasks 执行临界点]
container := &bulkContainer{
execute: execute,
maxTasks: options.cachedTasks,
}
// 2. 可以看出 bulkexecutor 底层依赖 periodicalexecutor
executor := &BulkExecutor{
executor: NewPeriodicalExecutor(options.flushInterval, container),
container: container,
}
return executor
}
```
而这个 `container`是个 `interface`
```go
TaskContainer interface {
// 把 task 加入 container
AddTask(task interface{}) bool
// 实际上是去执行传入的 execute func()
Execute(tasks interface{})
// 达到临界值,移除 container 中全部的 task通过 channel 传递到 execute func() 执行
RemoveAll() interface{}
}
```
由此可见之间的依赖关系:
- `bulkexecutor``periodicalexecutor` + `bulkContainer`
- `chunkexecutor``periodicalexecutor` + `chunkContainer`
> [!TIP]
> 所以你想完成自己的 `executor`,可以实现 `container` 的这 3 个接口,再结合 `periodicalexecutor` 就行
所以回到👆那张图,我们的重点就放在 `periodicalexecutor`,看看它是怎么设计的?
## 如何使用
首先看看如何在业务中使用这个组件:
现有一个定时服务,每天固定时间去执行从 `mysql``clickhouse` 的数据同步:
```go
type DailyTask struct {
ckGroup *clickhousex.Cluster
insertExecutor *executors.BulkExecutor
mysqlConn sqlx.SqlConn
}
```
初始化 `bulkExecutor`
```go
func (dts *DailyTask) Init() {
// insertIntoCk() 是真正insert执行函数【需要开发者自己编写具体业务逻辑】
dts.insertExecutor = executors.NewBulkExecutor(
dts.insertIntoCk,
executors.WithBulkInterval(time.Second*3), // 3s会自动刷一次container中task去执行
executors.WithBulkTasks(10240), // container最大task数。一般设为2的幂次
)
}
```
> [!TIP]
> 额外介绍一下:`clickhouse`  适合大批量的插入,因为 insert 速度很快,大批量 insert 更能充分利用 clickhouse
主体业务逻辑编写:
```go
func (dts *DailyTask) insertNewData(ch chan interface{}, sqlFromDb *model.Task) error {
for item := range ch {
if r, vok := item.(*model.Task); !vok {
continue
}
err := dts.insertExecutor.Add(r)
if err != nil {
r.Tag = sqlFromDb.Tag
r.TagId = sqlFromDb.Id
r.InsertId = genInsertId()
r.ToRedis = toRedis == constant.INCACHED
r.UpdateWay = sqlFromDb.UpdateWay
// 1. Add Task
err := dts.insertExecutor.Add(r)
if err != nil {
logx.Error(err)
}
}
}
// 2. Flush Task container
dts.insertExecutor.Flush()
// 3. Wait All Task Finish
dts.insertExecutor.Wait()
}
```
> [!TIP]
> 可能会疑惑为什么要 `Flush(), Wait()` ,后面会通过源码解析一下
使用上总体分为 3 步:
- `Add()`:加入 task
- `Flush()`:刷新 `container` 中的 task
- `Wait()`:等待全部 task 执行完成
## 源码分析
> [!TIP]
> 此处主要分析 `periodicalexecutor`,因为其他两个常用的 `executor` 都依赖它
### 初始化
```go
func New...(interval time.Duration, container TaskContainer) *PeriodicalExecutor {
executor := &PeriodicalExecutor{
commander: make(chan interface{}, 1),
interval: interval,
container: container,
confirmChan: make(chan lang.PlaceholderType),
newTicker: func(d time.Duration) timex.Ticker {
return timex.NewTicker(interval)
},
}
...
return executor
}
```
- `commander`:传递 `tasks` 的 channel
- `container`:暂存 `Add()` 的 task
- `confirmChan`:阻塞 `Add()` ,在开始本次的 `executeTasks()` 会放开阻塞
- `ticker`:定时器,防止 `Add()` 阻塞时,会有一个定时执行的机会,及时释放暂存的 task
### Add()
初始化完,在业务逻辑的第一步就是把 task 加入 `executor`
```go
func (pe *PeriodicalExecutor) Add(task interface{}) {
if vals, ok := pe.addAndCheck(task); ok {
pe.commander <- vals
<-pe.confirmChan
}
}
func (pe *PeriodicalExecutor) addAndCheck(task interface{}) (interface{}, bool) {
pe.lock.Lock()
defer func() {
// 一开始为 false
var start bool
if !pe.guarded {
// backgroundFlush() 会将 guarded 重新置反
pe.guarded = true
start = true
}
pe.lock.Unlock()
// 在第一条 task 加入的时候就会执行 if 中的 backgroundFlush()。后台协程刷task
if start {
pe.backgroundFlush()
}
}()
// 控制maxTask>=maxTask 将container中tasks pop, return
if pe.container.AddTask(task) {
return pe.container.RemoveAll(), true
}
return nil, false
}
```
`addAndCheck()``AddTask()` 就是在控制最大 tasks 数,如果超过就执行 `RemoveAll()` ,将暂存 `container` 的 tasks pop传递给 `commander` ,后面有 goroutine 循环读取,然后去执行 tasks。
### backgroundFlush()
开启一个后台协程,对 `container` 中的 task不断刷新
```go
func (pe *PeriodicalExecutor) backgroundFlush() {
// 封装 go func(){}
threading.GoSafe(func() {
ticker := pe.newTicker(pe.interval)
defer ticker.Stop()
var commanded bool
last := timex.Now()
for {
select {
// 从channel拿到 []tasks
case vals := <-pe.commander:
commanded = true
// 实质wg.Add(1)
pe.enterExecution()
// 放开 Add() 的阻塞,而且此时暂存区也为空。才开始新的 task 加入
pe.confirmChan <- lang.Placeholder
// 真正的执行 task 逻辑
pe.executeTasks(vals)
last = timex.Now()
case <-ticker.Chan():
if commanded {
// 由于select选择的随机性如果同时满足两个条件同时执行完上面的此处置反并跳过本段执行
// https://draveness.me/golang/docs/part2-foundation/ch05-keyword/golang-select/
commanded = false
} else if pe.Flush() {
// 刷新完成,定时器清零。暂存区空了,开始下一次定时刷新
last = timex.Now()
} else if timex.Since(last) > pe.interval*idleRound {
// 既没到maxTaskFlush() err并且 last->now 时间过长,会再次触发 Flush()
// 只有这置反,才会开启一个新的 backgroundFlush() 后台协程
pe.guarded = false
// 再次刷新,防止漏掉
pe.Flush()
return
}
}
}
})
}
```
总体两个过程:
- `commander` 接收到 `RemoveAll()` 传递来的 tasks然后执行并放开 `Add()` 的阻塞,得以继续 `Add()`
- `ticker` 到时间了,如果第一步没有执行,则自动 `Flush()` ,也会去做 task 的执行
### Wait()
`backgroundFlush()` ,提到一个函数:`enterExecution()`
```go
func (pe *PeriodicalExecutor) enterExecution() {
pe.wgBarrier.Guard(func() {
pe.waitGroup.Add(1)
})
}
func (pe *PeriodicalExecutor) Wait() {
pe.wgBarrier.Guard(func() {
pe.waitGroup.Wait()
})
}
```
这样列举就知道为什么之前在最后要带上 `dts.insertExecutor.Wait()`,当然要等待全部的 `goroutine task` 完成。
## 思考
在看源码中,思考了一些其他设计上的思路,大家是否也有类似的问题:
- 在分析 `executors` 中,会发现很多地方都有 `lock`
> [!TIP]
> `go test` 存在竞态,使用加锁来避免这种情况
- 在分析 `confirmChan` 时发现,`confirmChan` 在此次[提交](https://github.com/tal-tech/go-zero/commit/9d9399ad1014c171cc9bd9c87f78b5d2ac238ce4)才出现,为什么会这么设计?
> 之前是:`wg.Add(1)` 是写在 `executeTasks()` ;现在是:先`wg.Add(1)`,再放开 `confirmChan` 阻塞
> 如果 `executor func` 执行阻塞,`Add task` 还在进行,因为没有阻塞,可能很快执行到 `Executor.Wait()`,这时就会出现 `wg.Wait()``wg.Add()` 前执行,这会 `panic`
具体可以看最新版本的`TestPeriodicalExecutor_WaitFast()` ,不妨跑在此版本上,就可以重现
## 总结
剩余还有几个 `executors` 的分析,就留给大家去看看源码。
总之,整体设计上:
- 遵循面向接口设计
- 灵活使用 `channel` `waitgroup` 等并发工具
- 执行单元+存储单元的搭配使用
`go-zero` 中还有很多实用的组件工具,用好工具对于提升服务性能和开发效率都有很大的帮助,希望本篇文章能给大家带来一些收获。

View File

@ -0,0 +1,30 @@
# 扩展阅读
扩展阅读是对[go-zero](https://github.com/zeromicro/go-zero) 中的最佳实现和组件的介绍,
因此会比较庞大,而此资源将会持续更新,也欢迎大家来进行文档贡献,本节将包含以下目录(按照文档更新时间排序):
* [快速构建高并发微服务](shorturl.md)
* [日志组件介绍](logx.md)
* [布隆过滤器](bloom.md)
* [executors](executors.md)
* [流处理组件 fx](fx.md)
* [go-zero mysql使用介绍](mysql.md)
* [redis锁](redis-lock.md)
* [periodlimit限流](periodlimit.md)
* [令牌桶限流](tokenlimit.md)
* [时间轮介绍](timing-wheel.md)
* [熔断原理与实现](breaker-algorithms.md)
* [进程内缓存组件 collection.Cache](collection.md)
* [高效的关键词替换和敏感词过滤工具](keywords.md)
* [服务自适应降载保护设计](loadshedding.md)
* [文本序列化和反序列化](mapping.md)
* [并发处理工具 MapReduce](mapreduce.md)
* [基于prometheus的微服务指标监控](metric.md)
* [防止缓存击穿之进程内共享调用](sharedcalls.md)
* [DB缓存机制](sql-cache.md)
* [zrpc 使用介绍](zrpc.md)
* [go-zero缓存设计之持久层缓存](redis-cache.md)
* [go-zero缓存设计之业务层缓存](buiness-cache.md)
* [go-zero分布式定时任务](go-queue.md)
* [我是如何用go-zero 实现一个中台系统](datacenter.md)
* [流数据处理利器](stream.md)
* [10月3日线上交流问题汇总](online-exchange.md)

48
go-zero.dev/cn/faq.md Normal file
View File

@ -0,0 +1,48 @@
# 常见问题集合
1. goctl安装了执行命令却提示 `command not found: goctl` 字样。
> 如果你通过 `go get` 方式安装,那么 `goctl` 应该位于 `$GOPATH` 中,
> 你可以通过 `go env GOPATH` 查看完整路径,不管你的 `goctl` 是在 `$GOPATH`中,
> 还是在其他目录,出现上述问题的原因就是 `goctl` 所在目录不在 `PATH` (环境变量)中所致。
2. rpc怎么调用
> 该问题可以参考快速开始中的[rpc编写与调用](rpc-call.md)介绍其中有rpc调用的使用逻辑。
3. proto使用了importgoctl命令需要怎么写。
> `goctl` 对于import的proto指定 `BasePath` 提供了 `protoc` 的flag映射`--proto_path, -I`
> `goctl` 会将此flag值传递给 `protoc`.
4. 假设 `base.proto` 的被main proto 引入了,为什么不生能生成`base.pb.go`。
> 对于 `base.proto` 这种类型的文件一般都是开发者有message复用的需求他的来源不止有开发者自己编写的`proto`文件,
> 还有可能来源于 `google.golang.org/grpc` 中提供的一些基本的proto,比如 `google/protobuf/any.proto`, 如果由 `goctl`
> 来生成那么就失去了集中管理这些proto的意义。
5. model怎么控制缓存时间
> 在 `sqlc.NewNodeConn` 的时候可以通过可选参数 `cache.WithExpiry` 传递如缓存时间控制为1天代码如下:
> ```go
> sqlc.NewNodeConn(conn,redis,cache.WithExpiry(24*time.Hour))
> ```
6. jwt鉴权怎么实现
> 请参考[jwt鉴权](jwt.md)
7. api中间件怎么使用
> 请参考[中间件](middleware.md)
8. 怎么关闭输出的统计日志(stat)
> logx.DisableStat()
9. rpc直连与服务发现连接模式写法
```go
// mode1: 集群直连
// conf:=zrpc.NewDirectClientConf([]string{"ip:port"},"app","token")
// mode2: etcd 服务发现
// conf:=zrpc.NewEtcdClientConf([]string{"ip:port"},"key","app","token")
// client, _ := zrpc.NewClient(conf)
// mode3: ip直连mode
// client, _ := zrpc.NewClientWithTarget("127.0.0.1:8888")
```
faq会不定期更新大家遇到的问题也欢迎大家把常见问题通过pr写在这里。

View File

@ -0,0 +1,11 @@
# 框架设计
![整体框架](./resource/architechture.svg)
本节将从 go-zero 的设计理念go-zero 服务的最佳实践目录来说明 go-zero 框架的设计,本节将包含以下小节:
* [go-zero设计理念](go-zero-design.md)
* [go-zero特点](go-zero-features.md)
* [api语法介绍](api-grammar.md)
* [api目录结构](api-dir.md)
* [rpc目录结构](rpc-dir.md)

314
go-zero.dev/cn/go-queue.md Normal file
View File

@ -0,0 +1,314 @@
* ### go-zero 分布式定时任务
日常任务开发中我们会有很多异步、批量、定时、延迟任务要处理go-zero中有go-queue推荐使用go-queue去处理go-queue本身也是基于go-zero开发的其本身是有两种模式
- dq : 依赖于beanstalkd分布式可存储延迟、定时设置关机重启可以重新执行消息不会丢失使用非常简单go-queue中使用了redis setnx保证了每条消息只被消费一次使用场景主要是用来做日常任务使用
- kq依赖于kafka这个就不多介绍啦大名鼎鼎的kafka使用场景主要是做消息队列
我们主要说一下dqkq使用也一样的只是依赖底层不同如果没使用过beanstalkd没接触过beanstalkd的可以先google一下使用起来还是挺容易的。
etc/job.yaml : 配置文件
```yaml
Name: job
Log:
ServiceName: job
Level: info
#dq依赖Beanstalks、redis Beanstalks配置、redis配置
DqConf:
Beanstalks:
- Endpoint: 127.0.0.1:7771
Tube: tube1
- Endpoint: 127.0.0.1:7772
Tube: tube2
Redis:
Host: 127.0.0.1:6379
Type: node
```
Internal/config/config.go 解析dq对应etc/*.yaml配置
```go
/**
* @Description 配置文件
* @Author Mikael
* @Email 13247629622@163.com
* @Date 2021/1/18 12:05
* @Version 1.0
**/
package config
import (
"github.com/tal-tech/go-queue/dq"
"github.com/tal-tech/go-zero/core/service"
)
type Config struct {
service.ServiceConf
DqConf dq.DqConf
}
```
Handler/router.go : 负责注册多任务
```go
/**
* @Description 注册job
* @Author Mikael
* @Email 13247629622@163.com
* @Date 2021/1/18 12:05
* @Version 1.0
**/
package handler
import (
"context"
"github.com/tal-tech/go-zero/core/service"
"job/internal/logic"
"job/internal/svc"
)
func RegisterJob(serverCtx *svc.ServiceContext,group *service.ServiceGroup) {
group.Add(logic.NewProducerLogic(context.Background(),serverCtx))
group.Add(logic.NewConsumerLogic(context.Background(),serverCtx))
group.Start()
}
```
ProducerLogic: 其中一个job业务逻辑
```go
/**
* @Description 生产者任务
* @Author Mikael
* @Email 13247629622@163.com
* @Date 2021/1/18 12:05
* @Version 1.0
**/
package logic
import (
"context"
"github.com/tal-tech/go-queue/dq"
"github.com/tal-tech/go-zero/core/logx"
"github.com/tal-tech/go-zero/core/threading"
"job/internal/svc"
"strconv"
"time"
)
type Producer struct {
ctx context.Context
svcCtx *svc.ServiceContext
logx.Logger
}
func NewProducerLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Producer {
return &Producer{
ctx: ctx,
svcCtx: svcCtx,
Logger: logx.WithContext(ctx),
}
}
func (l *Producer)Start() {
logx.Infof("start Producer \n")
threading.GoSafe(func() {
producer := dq.NewProducer([]dq.Beanstalk{
{
Endpoint: "localhost:7771",
Tube: "tube1",
},
{
Endpoint: "localhost:7772",
Tube: "tube2",
},
})
for i := 1000; i < 1005; i++ {
_, err := producer.Delay([]byte(strconv.Itoa(i)), time.Second * 1)
if err != nil {
logx.Error(err)
}
}
})
}
func (l *Producer)Stop() {
logx.Infof("stop Producer \n")
}
```
另外一个Job业务逻辑
```go
/**
* @Description 消费者任务
* @Author Mikael
* @Email 13247629622@163.com
* @Date 2021/1/18 12:05
* @Version 1.0
**/
package logic
import (
"context"
"github.com/tal-tech/go-zero/core/logx"
"github.com/tal-tech/go-zero/core/threading"
"job/internal/svc"
)
type Consumer struct {
ctx context.Context
svcCtx *svc.ServiceContext
logx.Logger
}
func NewConsumerLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Consumer {
return &Consumer{
ctx: ctx,
svcCtx: svcCtx,
Logger: logx.WithContext(ctx),
}
}
func (l *Consumer)Start() {
logx.Infof("start consumer \n")
threading.GoSafe(func() {
l.svcCtx.Consumer.Consume(func(body []byte) {
logx.Infof("consumer job %s \n" ,string(body))
})
})
}
func (l *Consumer)Stop() {
logx.Infof("stop consumer \n")
}
```
svc/servicecontext.go
```go
/**
* @Description 配置
* @Author Mikael
* @Email 13247629622@163.com
* @Date 2021/1/18 12:05
* @Version 1.0
**/
package svc
import (
"job/internal/config"
"github.com/tal-tech/go-queue/dq"
)
type ServiceContext struct {
Config config.Config
Consumer dq.Consumer
}
func NewServiceContext(c config.Config) *ServiceContext {
return &ServiceContext{
Config: c,
Consumer: dq.NewConsumer(c.DqConf),
}
}
```
main.go启动文件
```go
/**
* @Description 启动文件
* @Author Mikael
* @Email 13247629622@163.com
* @Date 2021/1/18 12:05
* @Version 1.0
**/
package main
import (
"flag"
"fmt"
"github.com/tal-tech/go-zero/core/conf"
"github.com/tal-tech/go-zero/core/logx"
"github.com/tal-tech/go-zero/core/service"
"job/internal/config"
"job/internal/handler"
"job/internal/svc"
"os"
"os/signal"
"syscall"
"time"
)
var configFile = flag.String("f", "etc/job.yaml", "the config file")
func main() {
flag.Parse()
//配置
var c config.Config
conf.MustLoad(*configFile, &c)
ctx := svc.NewServiceContext(c)
//注册job
group := service.NewServiceGroup()
handler.RegisterJob(ctx,group)
//捕捉信号
ch := make(chan os.Signal)
signal.Notify(ch, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT)
for {
s := <-ch
logx.Info("get a signal %s", s.String())
switch s {
case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT:
fmt.Printf("stop group")
group.Stop()
logx.Info("job exit")
time.Sleep(time.Second)
return
case syscall.SIGHUP:
default:
return
}
}
}
```
#### 常见问题:
为什么使用`dp`,需要使用`redis`
- 因为`beanstalk`是单点服务,无法保证高可用。`dp`可以使用多个单点`beanstalk`服务,互相备份 & 保证高可用。使用`redis`解决重复消费问题。

View File

@ -0,0 +1,12 @@
# go-zero设计理念
对于微服务框架的设计,我们期望保障微服务稳定性的同时,也要特别注重研发效率。所以设计之初,我们就有如下一些准则:
* 保持简单,第一原则
* 弹性设计,面向故障编程
* 工具大于约定和文档
* 高可用
* 高并发
* 易扩展
* 对业务开发友好,封装复杂度
* 约束做一件事只有一种方式

View File

@ -0,0 +1,21 @@
# go-zero特性
go-zero 是一个集成了各种工程实践的包含 web 和 rpc 框架,有如下主要特点:
* 强大的工具支持,尽可能少的代码编写
* 极简的接口
* 完全兼容 net/http
* 支持中间件,方便扩展
* 高性能
* 面向故障编程,弹性设计
* 内建服务发现、负载均衡
* 内建限流、熔断、降载,且自动触发,自动恢复
* API 参数自动校验
* 超时级联控制
* 自动缓存控制
* 链路跟踪、统计报警等
* 高并发支撑,稳定保障了疫情期间每天的流量洪峰
如下图,我们从多个层面保障了整体服务的高可用:
![弹性设计](https://gitee.com/kevwan/static/raw/master/doc/images/resilience.jpg)

View File

@ -0,0 +1,69 @@
# api命令
goctl api是goctl中的核心模块之一其可以通过.api文件一键快速生成一个api服务如果仅仅是启动一个go-zero的api演示项目
你甚至都不用编码就可以完成一个api服务开发及正常运行。在传统的api项目中我们要创建各级目录编写结构体
定义路由添加logic文件这一系列操作如果按照一条协议的业务需求计算整个编码下来大概需要56分钟才能真正进入业务逻辑的编写
这还不考虑编写过程中可能产生的各种错误,而随着服务的增多,随着协议的增多,这部分准备工作的时间将成正比上升,
而goctl api则可以完全替代你去做这一部分工作不管你的协议要定多少个最终来说只需要花费10秒不到即可完成。
> [!TIP]
> 其中的结构体编写路由定义用api进行替代因此总的来说省去的是你创建文件夹、添加各种文件及资源依赖的过程的时间。
## api命令说明
```shell
$ goctl api -h
```
```text
NAME:
goctl api - generate api related files
USAGE:
goctl api command [command options] [arguments...]
COMMANDS:
new fast create api service
format format api files
validate validate api file
doc generate doc files
go generate go files for provided api in yaml file
java generate java files for provided api in api file
ts generate ts files for provided api in api file
dart generate dart files for provided api in api file
kt generate kotlin code for provided api file
plugin custom file generator
OPTIONS:
-o value the output api file
--help, -h show help
```
从上文中可以看到根据功能的不同api包含了很多的自命令和flag我们这里重点说明一下
`go`子命令其功能是生成golang api服务我们通过`goctl api go -h`看一下使用帮助:
```shell
$ goctl api go -h
```
```text
NAME:
goctl api go - generate go files for provided api in yaml file
USAGE:
goctl api go [command options] [arguments...]
OPTIONS:
--dir value the target dir
--api value the api file
--style value the file naming format, see [https://github.com/tal-tech/go-zero/tree/master/tools/goctl/config/readme.md]
```
* --dir 代码输出目录
* --api 指定api源文件
* --style 指定生成代码文件的文件名称风格,详情见[文件名称命名style说明](https://github.com/tal-tech/go-zero/tree/master/tools/goctl/config/readme.md)
## 使用示例
```shell
$ goctl api go -api user.api -dir . -style gozero
```
# 猜你想看
* [api语法](api-grammar.md)
* [api目录](api-dir.md)

View File

@ -0,0 +1,271 @@
# goctl命令大全
![goctl](https://zeromicro.github.io/go-zero/cn/resource/goctl-command.png)
# goctl
## api
(api服务相关操作)
### -o
(生成api文件)
- 示例goctl api -o user.api
### new
(快速创建一个api服务)
- 示例goctl api new user
### format
(api格式化vscode使用)
- -dir
(目标目录)
- -iu
(是否自动更新goctl)
- -stdin
(是否从标准输入读取数据)
### validate
(验证api文件是否有效)
- -api
(指定api文件源)
- 示例goctl api validate -api user.api
### doc
(生成doc markdown)
- -dir
(指定目录)
- 示例goctl api doc -dir user
### go
(生成golang api服务)
- -dir
(指定代码存放目录)
- -api
(指定api文件源)
- -force
(是否强制覆盖已经存在的文件)
- -style
(指定文件名命名风格gozero:小写go_zero:下划线,GoZero:驼峰)
### java
(生成访问api服务代码-java语言)
- -dir
(指定代码存放目录)
- -api
(指定api文件源)
### ts
(生成访问api服务代码-ts语言)
- -dir
(指定代码存放目录)
- -api
(指定api文件源)
- webapi
- caller
- unwrap
### dart
(生成访问api服务代码-dart语言)
- -dir
(指定代码存放目标)
- -api
(指定api文件源)
### kt
(生成访问api服务代码-kotlin语言)
- -dir
(指定代码存放目标)
- -api
(指定api文件源)
- -pkg
(指定包名)
### plugin
- -plugin
可执行文件
- -dir
代码存放目标文件夹
- -api
api源码文件
- -style
文件名命名格式化
## template
(模板操作)
### init
(缓存api/rpc/model模板)
- 示例goctl template init
### clean
(清空缓存模板)
- 示例goctl template clean
### update
(更新模板)
- -category,c
(指定需要更新的分组名 api|rpc|model)
- 示例goctl template update -c api
### revert
(还原指定模板文件)
- -category,c
(指定需要更新的分组名 api|rpc|model)
- -name,n
(指定模板文件名)
## config
(配置文件生成)
### -path,p
(指定配置文件存放目录)
- 示例goctl config -p user
## docker
(生成Dockerfile)
### -go
(指定main函数文件)
### -port
(指定暴露端口)
## rpc (rpc服务相关操作)
### new
(快速生成一个rpc服务)
- -idea
(标识命令是否来源于idea插件用于idea插件开发使用终端执行请忽略[可选参数])
- -style
(指定文件名命名风格gozero:小写go_zero:下划线,GoZero:驼峰)
### template
(创建一个proto模板文件)
- -idea
(标识命令是否来源于idea插件用于idea插件开发使用终端执行请忽略[可选参数])
- -out,o
(指定代码存放目录)
### proto
(根据proto生成rpc服务)
- -src,s
(指定proto文件源)
- -proto_path,I
(指定proto import查找目录protoc原生命令具体用法可参考protoc -h查看)
- -dir,d
(指定代码存放目录)
- -idea
(标识命令是否来源于idea插件用于idea插件开发使用终端执行请忽略[可选参数])
- -style
(指定文件名命名风格gozero:小写go_zero:下划线,GoZero:驼峰)
### model
(model层代码操作)
- mysql
(从mysql生成model代码)
- ddl
(指定数据源为
ddl文件生成model代码)
- -src,s
(指定包含ddl的sql文件源支持通配符匹配)
- -dir,d
(指定代码存放目录)
- -style
(指定文件名命名风格gozero:小写go_zero:下划线,GoZero:驼峰)
- -cache,c
(生成代码是否带redis缓存逻辑bool值)
- -idea
(标识命令是否来源于idea插件用于idea插件开发使用终端执行请忽略[可选参数])
- datasource
(指定数据源从
数据库链接生成model代码)
- -url
(指定数据库链接)
- -table,t
(指定表名,支持通配符)
- -dir,d
(指定代码存放目录)
- -style
(指定文件名命名风格gozero:小写go_zero:下划线,GoZero:驼峰)
- -cache,c
(生成代码是否带redis缓存逻辑bool值)
- -idea
(标识命令是否来源于idea插件用于idea插件开发使用终端执行请忽略[可选参数])
- mongo
(从mongo生成model代码)
- -type,t
(指定Go Type名称)
- -cache,c
(生成代码是否带redis缓存逻辑bool值默认否)
- -dir,d
(指定代码生成目录)
- -style
(指定文件名命名风格gozero:小写go_zero:下划线,GoZero:驼峰)
## upgrade
goctl更新到最新版本
## kube
生成k8s部署文件
### deploy
- -name
服务名称
- -namespace
指定k8s namespace
- -image
指定镜像名称
- -secret
指定获取镜像的k8s secret
- -requestCpu
指定cpu默认分配额
- -requestMem
指定内存默认分配额
- -limitCpu
指定cpu最大分配额
- -limitMem
指定内存最大分配额
- -o
deployment.yaml输出目录
- -replicas
指定副本数
- -revisions
指定保留发布记录数
- -port
指定服务端口
- -nodePort
指定服务对外暴露端口
- -minReplicas
指定最小副本数
- -maxReplicas
指定最大副本数

View File

@ -0,0 +1,34 @@
# Goctl安装
## 前言
Goctl在go-zero项目开发着有着很大的作用其可以有效的帮助开发者大大提高开发效率减少代码的出错率缩短业务开发的工作量更多的Goctl的介绍请阅读[Goctl介绍](goctl.md),
在这里我们强烈推荐大家安装因为后续演示例子中我们大部分都会以goctl进行演示。
## 安装(mac&linux)
* download&install
```shell
GO111MODULE=on GOPROXY=https://goproxy.cn/,direct go get -u github.com/tal-tech/go-zero/tools/goctl
```
* 环境变量检测
`go get`下载编译后的二进制文件位于`$GOPATH/bin`目录下,要确保`$GOPATH/bin`已经添加到环境变量。
```shell
$ sudo vim /etc/paths
```
在最后一行添加如下内容
```text
$GOPATH/bin
```
> [!TIP]
> `$GOPATH`为你本机上的文件地址
* 安装结果验证
```shell
$ goctl -v
```
```text
goctl version 1.1.4 darwin/amd64
```
> [!TIP]
> windows用户添加环境变量请自行google

View File

@ -0,0 +1,374 @@
# model命令
goctl model 为go-zero下的工具模块中的组件之一目前支持识别mysql ddl进行model层代码生成通过命令行或者idea插件即将支持可以有选择地生成带redis cache或者不带redis cache的代码逻辑。
## 快速开始
* 通过ddl生成
```shell
$ goctl model mysql ddl -src="./*.sql" -dir="./sql/model" -c
```
执行上述命令后即可快速生成CURD代码。
```text
model
│   ├── error.go
│   └── usermodel.go
```
* 通过datasource生成
```shell
$ goctl model mysql datasource -url="user:password@tcp(127.0.0.1:3306)/database" -table="*" -dir="./model"
```
* 生成代码示例
```go
package model
import (
"database/sql"
"fmt"
"strings"
"time"
"github.com/tal-tech/go-zero/core/stores/cache"
"github.com/tal-tech/go-zero/core/stores/sqlc"
"github.com/tal-tech/go-zero/core/stores/sqlx"
"github.com/tal-tech/go-zero/core/stringx"
"github.com/tal-tech/go-zero/tools/goctl/model/sql/builderx"
)
var (
userFieldNames = builderx.RawFieldNames(&User{})
userRows = strings.Join(userFieldNames, ",")
userRowsExpectAutoSet = strings.Join(stringx.Remove(userFieldNames, "`id`", "`create_time`", "`update_time`"), ",")
userRowsWithPlaceHolder = strings.Join(stringx.Remove(userFieldNames, "`id`", "`create_time`", "`update_time`"), "=?,") + "=?"
cacheUserNamePrefix = "cache#User#name#"
cacheUserMobilePrefix = "cache#User#mobile#"
cacheUserIdPrefix = "cache#User#id#"
cacheUserPrefix = "cache#User#user#"
)
type (
UserModel interface {
Insert(data User) (sql.Result, error)
FindOne(id int64) (*User, error)
FindOneByUser(user string) (*User, error)
FindOneByName(name string) (*User, error)
FindOneByMobile(mobile string) (*User, error)
Update(data User) error
Delete(id int64) error
}
defaultUserModel struct {
sqlc.CachedConn
table string
}
User struct {
Id int64 `db:"id"`
User string `db:"user"` // 用户
Name string `db:"name"` // 用户名称
Password string `db:"password"` // 用户密码
Mobile string `db:"mobile"` // 手机号
Gender string `db:"gender"` // 男|女|未公开
Nickname string `db:"nickname"` // 用户昵称
CreateTime time.Time `db:"create_time"`
UpdateTime time.Time `db:"update_time"`
}
)
func NewUserModel(conn sqlx.SqlConn, c cache.CacheConf) UserModel {
return &defaultUserModel{
CachedConn: sqlc.NewConn(conn, c),
table: "`user`",
}
}
func (m *defaultUserModel) Insert(data User) (sql.Result, error) {
userNameKey := fmt.Sprintf("%s%v", cacheUserNamePrefix, data.Name)
userMobileKey := fmt.Sprintf("%s%v", cacheUserMobilePrefix, data.Mobile)
userKey := fmt.Sprintf("%s%v", cacheUserPrefix, data.User)
ret, err := m.Exec(func(conn sqlx.SqlConn) (result sql.Result, err error) {
query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?)", m.table, userRowsExpectAutoSet)
return conn.Exec(query, data.User, data.Name, data.Password, data.Mobile, data.Gender, data.Nickname)
}, userNameKey, userMobileKey, userKey)
return ret, err
}
func (m *defaultUserModel) FindOne(id int64) (*User, error) {
userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, id)
var resp User
err := m.QueryRow(&resp, userIdKey, func(conn sqlx.SqlConn, v interface{}) error {
query := fmt.Sprintf("select %s from %s where `id` = ? limit 1", userRows, m.table)
return conn.QueryRow(v, query, id)
})
switch err {
case nil:
return &resp, nil
case sqlc.ErrNotFound:
return nil, ErrNotFound
default:
return nil, err
}
}
func (m *defaultUserModel) FindOneByUser(user string) (*User, error) {
userKey := fmt.Sprintf("%s%v", cacheUserPrefix, user)
var resp User
err := m.QueryRowIndex(&resp, userKey, m.formatPrimary, func(conn sqlx.SqlConn, v interface{}) (i interface{}, e error) {
query := fmt.Sprintf("select %s from %s where `user` = ? limit 1", userRows, m.table)
if err := conn.QueryRow(&resp, query, user); err != nil {
return nil, err
}
return resp.Id, nil
}, m.queryPrimary)
switch err {
case nil:
return &resp, nil
case sqlc.ErrNotFound:
return nil, ErrNotFound
default:
return nil, err
}
}
func (m *defaultUserModel) FindOneByName(name string) (*User, error) {
userNameKey := fmt.Sprintf("%s%v", cacheUserNamePrefix, name)
var resp User
err := m.QueryRowIndex(&resp, userNameKey, m.formatPrimary, func(conn sqlx.SqlConn, v interface{}) (i interface{}, e error) {
query := fmt.Sprintf("select %s from %s where `name` = ? limit 1", userRows, m.table)
if err := conn.QueryRow(&resp, query, name); err != nil {
return nil, err
}
return resp.Id, nil
}, m.queryPrimary)
switch err {
case nil:
return &resp, nil
case sqlc.ErrNotFound:
return nil, ErrNotFound
default:
return nil, err
}
}
func (m *defaultUserModel) FindOneByMobile(mobile string) (*User, error) {
userMobileKey := fmt.Sprintf("%s%v", cacheUserMobilePrefix, mobile)
var resp User
err := m.QueryRowIndex(&resp, userMobileKey, m.formatPrimary, func(conn sqlx.SqlConn, v interface{}) (i interface{}, e error) {
query := fmt.Sprintf("select %s from %s where `mobile` = ? limit 1", userRows, m.table)
if err := conn.QueryRow(&resp, query, mobile); err != nil {
return nil, err
}
return resp.Id, nil
}, m.queryPrimary)
switch err {
case nil:
return &resp, nil
case sqlc.ErrNotFound:
return nil, ErrNotFound
default:
return nil, err
}
}
func (m *defaultUserModel) Update(data User) error {
userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, data.Id)
_, err := m.Exec(func(conn sqlx.SqlConn) (result sql.Result, err error) {
query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, userRowsWithPlaceHolder)
return conn.Exec(query, data.User, data.Name, data.Password, data.Mobile, data.Gender, data.Nickname, data.Id)
}, userIdKey)
return err
}
func (m *defaultUserModel) Delete(id int64) error {
data, err := m.FindOne(id)
if err != nil {
return err
}
userNameKey := fmt.Sprintf("%s%v", cacheUserNamePrefix, data.Name)
userMobileKey := fmt.Sprintf("%s%v", cacheUserMobilePrefix, data.Mobile)
userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, id)
userKey := fmt.Sprintf("%s%v", cacheUserPrefix, data.User)
_, err = m.Exec(func(conn sqlx.SqlConn) (result sql.Result, err error) {
query := fmt.Sprintf("delete from %s where `id` = ?", m.table)
return conn.Exec(query, id)
}, userNameKey, userMobileKey, userIdKey, userKey)
return err
}
func (m *defaultUserModel) formatPrimary(primary interface{}) string {
return fmt.Sprintf("%s%v", cacheUserIdPrefix, primary)
}
func (m *defaultUserModel) queryPrimary(conn sqlx.SqlConn, v, primary interface{}) error {
query := fmt.Sprintf("select %s from %s where `id` = ? limit 1", userRows, m.table)
return conn.QueryRow(v, query, primary)
}
```
## 用法
```text
$ goctl model mysql -h
```
```text
NAME:
goctl model mysql - generate mysql model"
USAGE:
goctl model mysql command [command options] [arguments...]
COMMANDS:
ddl generate mysql model from ddl"
datasource generate model from datasource"
OPTIONS:
--help, -h show help
```
## 生成规则
* 默认规则
我们默认用户在建表时会创建createTime、updateTime字段(忽略大小写、下划线命名风格)且默认值均为`CURRENT_TIMESTAMP`而updateTime支持`ON UPDATE CURRENT_TIMESTAMP`,对于这两个字段生成`insert`、`update`时会被移除,不在赋值范畴内,当然,如果你不需要这两个字段那也无大碍。
* 带缓存模式
* ddl
```shell
$ goctl model mysql -src={patterns} -dir={dir} -cache
```
help
```
NAME:
goctl model mysql ddl - generate mysql model from ddl
USAGE:
goctl model mysql ddl [command options] [arguments...]
OPTIONS:
--src value, -s value the path or path globbing patterns of the ddl
--dir value, -d value the target dir
--style value the file naming format, see [https://github.com/tal-tech/go-zero/tree/master/tools/goctl/config/readme.md]
--cache, -c generate code with cache [optional]
--idea for idea plugin [optional]
```
* datasource
```shell
$ goctl model mysql datasource -url={datasource} -table={patterns} -dir={dir} -cache=true
```
help
```text
NAME:
goctl model mysql datasource - generate model from datasource
USAGE:
goctl model mysql datasource [command options] [arguments...]
OPTIONS:
--url value the data source of database,like "root:password@tcp(127.0.0.1:3306)/database
--table value, -t value the table or table globbing patterns in the database
--cache, -c generate code with cache [optional]
--dir value, -d value the target dir
--style value the file naming format, see [https://github.com/tal-tech/go-zero/tree/master/tools/goctl/config/readme.md]
--idea for idea plugin [optional]
```
> [!TIP]
> goctl model mysql ddl/datasource 均新增了一个`--style`参数,用于标记文件命名风格。
目前仅支持redis缓存如果选择带缓存模式即生成的`FindOne(ByXxx)`&`Delete`代码会生成带缓存逻辑的代码目前仅支持单索引字段除全文索引外对于联合索引我们默认认为不需要带缓存且不属于通用型代码因此没有放在代码生成行列如example中user表中的`id`、`name`、`mobile`字段均属于单字段索引。
* 不带缓存模式
* ddl
```shell
$ goctl model -src={patterns} -dir={dir}
```
* datasource
```shell
$ goctl model mysql datasource -url={datasource} -table={patterns} -dir={dir}
```
or
* ddl
```shell
$ goctl model -src={patterns} -dir={dir}
```
* datasource
```shell
$ goctl model mysql datasource -url={datasource} -table={patterns} -dir={dir}
```
生成代码仅基本的CURD结构。
## 缓存
对于缓存这一块我选择用一问一答的形式进行罗列。我想这样能够更清晰的描述model中缓存的功能。
* 缓存会缓存哪些信息?
对于主键字段缓存,会缓存整个结构体信息,而对于单索引字段(除全文索引)则缓存主键字段值。
* 数据有更新(`update`)操作会清空缓存吗?
但仅清空主键缓存的信息why这里就不做详细赘述了。
* 为什么不按照单索引字段生成`updateByXxx`和`deleteByXxx`的代码?
理论上是没任何问题但是我们认为对于model层的数据操作均是以整个结构体为单位包括查询我不建议只查询某部分字段不反对否则我们的缓存就没有意义了。
* 为什么不支持`findPageLimit`、`findAll`这么模式代码生层?
目前我认为除了基本的CURD外其他的代码均属于<i>业务型</i>代码,这个我觉得开发人员根据业务需要进行编写更好。
# 类型转换规则
| mysql dataType | golang dataType | golang dataType(if null&&default null) |
|----------------|-----------------|----------------------------------------|
| bool | int64 | sql.NullInt64 |
| boolean | int64 | sql.NullInt64 |
| tinyint | int64 | sql.NullInt64 |
| smallint | int64 | sql.NullInt64 |
| mediumint | int64 | sql.NullInt64 |
| int | int64 | sql.NullInt64 |
| integer | int64 | sql.NullInt64 |
| bigint | int64 | sql.NullInt64 |
| float | float64 | sql.NullFloat64 |
| double | float64 | sql.NullFloat64 |
| decimal | float64 | sql.NullFloat64 |
| date | time.Time | sql.NullTime |
| datetime | time.Time | sql.NullTime |
| timestamp | time.Time | sql.NullTime |
| time | string | sql.NullString |
| year | time.Time | sql.NullInt64 |
| char | string | sql.NullString |
| varchar | string | sql.NullString |
| binary | string | sql.NullString |
| varbinary | string | sql.NullString |
| tinytext | string | sql.NullString |
| text | string | sql.NullString |
| mediumtext | string | sql.NullString |
| longtext | string | sql.NullString |
| enum | string | sql.NullString |
| set | string | sql.NullString |
| json | string | sql.NullString |

View File

@ -0,0 +1,308 @@
# 其他命令
* goctl docker
* goctl kube
## goctl docker
`goctl docker` 可以极速生成一个 Dockerfile帮助开发/运维人员加快部署节奏,降低部署复杂度。
### 准备工作
* docker安装
### Dockerfile 额外注意点
* 选择最简单的镜像比如alpine整个镜像5M左右
* 设置镜像时区
```shell
RUN apk add --no-cache tzdata
ENV TZ Asia/Shanghai
```
### 多阶段构建
* 第一阶段构建出可执行文件,确保构建过程独立于宿主机
* 第二阶段将第一阶段的输出作为输入,构建出最终的极简镜像
### Dockerfile编写过程
* 首先安装 goctl 工具
```shell
$ GO111MODULE=on GOPROXY=https://goproxy.cn/,direct go get -u github.com/tal-tech/go-zero/tools/goctl
```
* 在 greet 项目下创建一个 hello 服务
```shell
$ goctl api new hello
```
文件结构如下:
```text
greet
├── go.mod
├── go.sum
└── service
└── hello
├── Dockerfile
├── etc
│ └── hello-api.yaml
├── hello.api
├── hello.go
└── internal
├── config
│ └── config.go
├── handler
│ ├── hellohandler.go
│ └── routes.go
├── logic
│ └── hellologic.go
├── svc
│ └── servicecontext.go
└── types
└── types.go
```
* 在 `hello` 目录下一键生成 `Dockerfile`
```shell
$ goctl docker -go hello.go
```
Dockerfile 内容如下:
```shell
FROM golang:alpine AS builder
LABEL stage=gobuilder
ENV CGO_ENABLED 0
ENV GOOS linux
ENV GOPROXY https://goproxy.cn,direct
WORKDIR /build/zero
ADD go.mod .
ADD go.sum .
RUN go mod download
COPY . .
COPY service/hello/etc /app/etc
RUN go build -ldflags="-s -w" -o /app/hello service/hello/hello.go
FROM alpine
RUN apk update --no-cache
RUN apk add --no-cache ca-certificates
RUN apk add --no-cache tzdata
ENV TZ Asia/Shanghai
WORKDIR /app
COPY --from=builder /app/hello /app/hello
COPY --from=builder /app/etc /app/etc
CMD ["./hello", "-f", "etc/hello-api.yaml"]
```
* 在 `hello` 目录下 `build` 镜像
```shell
$ docker build -t hello:v1 -f service/hello/Dockerfile .
```
* 查看镜像
```shell
hello v1 5455f2eaea6b 7 minutes ago 18.1MB
```
可以看出镜像大小约为18M。
* 启动服务
```shell
$ docker run --rm -it -p 8888:8888 hello:v1
```
* 测试服务
```shell
$ curl -i http://localhost:8888/from/you
```
```text
HTTP/1.1 200 OK
Content-Type: application/json
Date: Thu, 10 Dec 2020 06:03:02 GMT
Content-Length: 14
{"message":""}
```
### goctl docker总结
goctl 工具极大简化了 Dockerfile 文件的编写,提供了开箱即用的最佳实践,并且支持了模板自定义。
## goctl kube
`goctl kube`提供了快速生成一个 `k8s` 部署文件的功能,可以加快开发/运维人员的部署进度,减少部署复杂度。
### 头疼编写 K8S 部署文件?
- `K8S yaml` 参数很多,需要边写边查?
- 保留回滚版本数怎么设?
- 如何探测启动成功,如何探活?
- 如何分配和限制资源?
- 如何设置时区?否则打印日志是 GMT 标准时间
- 如何暴露服务供其它服务调用?
- 如何根据 CPU 和内存使用率来配置水平伸缩?
首先,你需要知道有这些知识点,其次要把这些知识点都搞明白也不容易,再次,每次编写依然容易出错!
## 创建服务镜像
为了演示,这里我们以 `redis:6-alpine` 镜像为例。
## 完整 K8S 部署文件编写过程
- 首先安装 `goctl` 工具
```shell
$ GO111MODULE=on GOPROXY=https://goproxy.cn/,direct go get -u github.com/tal-tech/go-zero/tools/goctl
```
- 一键生成 K8S 部署文件
```shell
$ goctl kube deploy -name redis -namespace adhoc -image redis:6-alpine -o redis.yaml -port 6379
```
生成的 `yaml` 文件如下:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
namespace: adhoc
labels:
app: redis
spec:
replicas: 3
revisionHistoryLimit: 5
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:6-alpine
lifecycle:
preStop:
exec:
command: ["sh","-c","sleep 5"]
ports:
- containerPort: 6379
readinessProbe:
tcpSocket:
port: 6379
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
tcpSocket:
port: 6379
initialDelaySeconds: 15
periodSeconds: 20
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: 1000m
memory: 1024Mi
volumeMounts:
- name: timezone
mountPath: /etc/localtime
volumes:
- name: timezone
hostPath:
path: /usr/share/zoneinfo/Asia/Shanghai
---
apiVersion: v1
kind: Service
metadata:
name: redis-svc
namespace: adhoc
spec:
ports:
- port: 6379
selector:
app: redis
---
apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
name: redis-hpa-c
namespace: adhoc
labels:
app: redis-hpa-c
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: redis
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
targetAverageUtilization: 80
---
apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
name: redis-hpa-m
namespace: adhoc
labels:
app: redis-hpa-m
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: redis
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: memory
targetAverageUtilization: 80
```
- 部署服务,如果 `adhoc` namespace 不存在的话,请先通过 `kubectl create namespace adhoc` 创建
```
$ kubectl apply -f redis.yaml
deployment.apps/redis created
service/redis-svc created
horizontalpodautoscaler.autoscaling/redis-hpa-c created
horizontalpodautoscaler.autoscaling/redis-hpa-m created
```
- 查看服务允许状态
```
$ kubectl get all -n adhoc
NAME READY STATUS RESTARTS AGE
pod/redis-585bc66876-5ph26 1/1 Running 0 6m5s
pod/redis-585bc66876-bfqxz 1/1 Running 0 6m5s
pod/redis-585bc66876-vvfc9 1/1 Running 0 6m5s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/redis-svc ClusterIP 172.24.15.8 <none> 6379/TCP 6m5s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/redis 3/3 3 3 6m6s
NAME DESIRED CURRENT READY AGE
replicaset.apps/redis-585bc66876 3 3 3 6m6s
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
horizontalpodautoscaler.autoscaling/redis-hpa-c Deployment/redis 0%/80% 3 10 3 6m6s
horizontalpodautoscaler.autoscaling/redis-hpa-m Deployment/redis 0%/80% 3 10 3 6m6s
```
- 测试服务
```
$ kubectl run -i --tty --rm cli --image=redis:6-alpine -n adhoc -- sh
/data # redis-cli -h redis-svc
redis-svc:6379> set go-zero great
OK
redis-svc:6379> get go-zero
"great"
```
### goctl kube 总结
`goctl` 工具极大简化了 K8S yaml 文件的编写,提供了开箱即用的最佳实践,并且支持了模板自定义。
# 猜你想看
* [准备工作](prepare.md)
* [api目录](api-dir.md)
* [api语法](api-grammar.md)
* [api配置](api-config.md)
* [api命令介绍](goctl-api.md)
* [docker介绍](https://www.docker.com)
* [k8s介绍](https://kubernetes.io/zh/docs/home)

View File

@ -0,0 +1,62 @@
# plugin命令
goctl支持针对api自定义插件那我怎么来自定义一个插件了来看看下面最终怎么使用的一个例子。
```go
$ goctl api plugin -p goctl-android="android -package com.tal" -api user.api -dir .
```
上面这个命令可以分解成如下几步:
* goctl 解析api文件
* goctl 将解析后的结构 ApiSpec 和参数传递给goctl-android可执行文件
* goctl-android 根据 ApiSpec 结构体自定义生成逻辑。
此命令前面部分 goctl api plugin -p 是固定参数goctl-android="android -package com.tal" 是plugin参数其中goctl-android是插件二进制文件android -package com.tal是插件的自定义参数-api user.api -dir .是goctl通用自定义参数。
## 怎么编写自定义插件?
go-zero框架中包含了一个很简单的自定义插件 demo代码如下
```go
package main
import (
"fmt"
"github.com/tal-tech/go-zero/tools/goctl/plugin"
)
func main() {
plugin, err := plugin.NewPlugin()
if err != nil {
panic(err)
}
if plugin.Api != nil {
fmt.Printf("api: %+v \n", plugin.Api)
}
fmt.Printf("dir: %s \n", plugin.Dir)
fmt.Println("Enjoy anything you want.")
}
```
`plugin, err := plugin.NewPlugin()` 这行代码作用是解析从goctl传递过来的数据里面包含如下部分内容
```go
type Plugin struct {
Api *spec.ApiSpec
Style string
Dir string
}
```
> [!TIP]
> Api定义了api文件的结构数据
>
> Style可选参数可以用来控制文件命名规范
>
> Dir工作目录
完整的基于plugin实现的android plugin演示项目
[https://github.com/zeromicro/goctl-android](https://github.com/zeromicro/goctl-android)
# 猜你想看
* [api目录](api-dir.md)
* [api语法](api-grammar.md)
* [api配置](api-config.md)
* [api命令介绍](goctl-api.md)

View File

@ -1,4 +1,4 @@
# Rpc Generation
# rpc命令
Goctl Rpc是`goctl`脚手架下的一个rpc服务代码生成模块支持proto模板生成和rpc服务代码生成通过此工具生成代码你只需要关注业务逻辑编写而不用去编写一些重复性的代码。这使得我们把精力重心放在业务上从而加快了开发效率且降低了代码出错率。
@ -14,17 +14,17 @@ Goctl Rpc是`goctl`脚手架下的一个rpc服务代码生成模块支持prot
### 方式一快速生成greet服务
通过命令 `goctl rpc new ${servieName}`生成
通过命令 `goctl rpc new ${servieName}`生成
如生成greet rpc服务
如生成greet rpc服务
```Bash
goctl rpc new greet
```
执行后代码结构如下:
执行后代码结构如下:
```golang
```go
.
├── etc // yaml配置文件
│ └── greet.yaml
@ -69,7 +69,7 @@ if strings.ToLower(proto.Service.Name) == strings.ToLower(proto.GoPackage) {
}
```
rpc一键生成常见问题解决 <a href="#常见问题解决">常见问题解决</a>
rpc一键生成常见问题解决[常见错误处理](error.md)
### 方式二通过指定proto生成rpc服务
@ -79,11 +79,13 @@ rpc一键生成常见问题解决见 <a href="#常见问题解决">常见问
goctl rpc template -o=user.proto
```
```golang
```go
syntax = "proto3";
package remote;
option go_package = "remote";
message Request {
// 用户名
string username = 1;
@ -107,7 +109,7 @@ rpc一键生成常见问题解决见 <a href="#常见问题解决">常见问
* 生成rpc服务代码
```Bash
goctl rpc proto -src=user.proto
goctl rpc proto -src user.proto -dir .
```
## 准备工作
@ -135,6 +137,7 @@ OPTIONS:
--src value, -s value the file path of the proto source file
--proto_path value, -I value native command of protoc, specify the directory in which to search for imports. [optional]
--dir value, -d value the target path of the code
--style value the file naming format, see [https://github.com/tal-tech/go-zero/tree/master/tools/goctl/config/readme.md]
--idea whether the command execution environment is from idea plugin. [optional]
```
@ -143,6 +146,7 @@ OPTIONS:
* --src 必填proto数据源目前暂时支持单个proto文件生成
* --proto_path 可选protoc原生子命令用于指定proto import从何处查找可指定多个路径,如`goctl rpc -I={path1} -I={path2} ...`,在没有import时可不填。当前proto路径不用指定已经内置`-I`的详细用法请参考`protoc -h`
* --dir 可选默认为proto文件所在目录生成代码的目标目录
* --style 可选输出目录的文件命名风格详情见https://github.com/tal-tech/go-zero/tree/master/tools/goctl/config/readme.md
* --idea 可选是否为idea插件中执行终端执行可以忽略
@ -156,23 +160,17 @@ OPTIONS:
### 注意事项
* `google.golang.org/grpc`需要降级到v1.26.0,且protoc-gen-go版本不能高于v1.3.2see [https://github.com/grpc/grpc-go/issues/3347](https://github.com/grpc/grpc-go/issues/3347))即
```shell script
replace google.golang.org/grpc => google.golang.org/grpc v1.26.0
```
* proto不支持暂多文件同时生成
* proto暂不支持多文件同时生成
* proto不支持外部依赖包引入message不支持inline
* 目前main文件、shared文件、handler文件会被强制覆盖而和开发人员手动需要编写的则不会覆盖生成这一类在代码头部均有
```shell script
// Code generated by goctl. DO NOT EDIT!
// Source: xxx.proto
```
``` shell
// Code generated by goctl. DO NOT EDIT!
// Source: xxx.proto
```
的标识,请注意不要将也写业务性代码写在里面。
的标识,请注意不要将也写业务性代码写在里面。
## proto import
* 对于rpc中的requestType和returnType必须在main proto文件定义对于proto中的message可以像protoc一样import其他proto文件。
@ -180,11 +178,13 @@ OPTIONS:
proto示例:
### 错误import
```proto
```protobuf
syntax = "proto3";
package greet;
option go_package = "greet";
import "base/common.proto"
message Request {
@ -203,11 +203,13 @@ service Greet {
### 正确import
```proto
```protobuf
syntax = "proto3";
package greet;
option go_package = "greet";
import "base/common.proto"
message Request {
@ -223,43 +225,7 @@ service Greet {
}
```
## 常见问题解决(go mod工程)
* 错误一:
```golang
pb/xx.pb.go:220:7: undefined: grpc.ClientConnInterface
pb/xx.pb.go:224:11: undefined: grpc.SupportPackageIsVersion6
pb/xx.pb.go:234:5: undefined: grpc.ClientConnInterface
pb/xx.pb.go:237:24: undefined: grpc.ClientConnInterface
```
解决方法:请将`protoc-gen-go`版本降至v1.3.2及一下
* 错误二:
```golang
# go.etcd.io/etcd/clientv3/balancer/picker
../../../go/pkg/mod/go.etcd.io/etcd@v0.0.0-20200402134248-51bdeb39e698/clientv3/balancer/picker/err.go:25:9: cannot use &errPicker literal (type *errPicker) as type Picker in return argument:*errPicker does not implement Picker (wrong type for Pick method)
have Pick(context.Context, balancer.PickInfo) (balancer.SubConn, func(balancer.DoneInfo), error)
want Pick(balancer.PickInfo) (balancer.PickResult, error)
../../../go/pkg/mod/go.etcd.io/etcd@v0.0.0-20200402134248-51bdeb39e698/clientv3/balancer/picker/roundrobin_balanced.go:33:9: cannot use &rrBalanced literal (type *rrBalanced) as type Picker in return argument:
*rrBalanced does not implement Picker (wrong type for Pick method)
have Pick(context.Context, balancer.PickInfo) (balancer.SubConn, func(balancer.DoneInfo), error)
want Pick(balancer.PickInfo) (balancer.PickResult, error)
#github.com/tal-tech/go-zero/zrpc/internal/balancer/p2c
../../../go/pkg/mod/github.com/tal-tech/go-zero@v1.0.12/zrpc/internal/balancer/p2c/p2c.go:41:32: not enough arguments in call to base.NewBalancerBuilder
have (string, *p2cPickerBuilder)
want (string, base.PickerBuilder, base.Config)
../../../go/pkg/mod/github.com/tal-tech/go-zero@v1.0.12/zrpc/internal/balancer/p2c/p2c.go:58:9: cannot use &p2cPicker literal (type *p2cPicker) as type balancer.Picker in return argument:
*p2cPicker does not implement balancer.Picker (wrong type for Pick method)
have Pick(context.Context, balancer.PickInfo) (balancer.SubConn, func(balancer.DoneInfo), error)
want Pick(balancer.PickInfo) (balancer.PickResult, error)
```
解决方法:
```golang
replace google.golang.org/grpc => google.golang.org/grpc v1.26.0
```
# 猜你想看
* [rpc目录](rpc-dir.md)
* [rpc配置](rpc-config.md)
* [rpc调用](rpc-call.md)

69
go-zero.dev/cn/goctl.md Normal file
View File

@ -0,0 +1,69 @@
# Goctl
goctl是go-zero微服务框架下的代码生成工具。使用 goctl 可显著提升开发效率,让开发人员将时间重点放在业务开发上,其功能有:
- api服务生成
- rpc服务生成
- model代码生成
- 模板管理
本节将包含以下内容:
* [命令大全](goctl-commands.md)
* [api命令](goctl-api.md)
* [rpc命令](goctl-rpc.md)
* [model命令](goctl-model.md)
* [plugin命令](goctl-plugin.md)
* [其他命令](goctl-other.md)
## goctl 读音
很多人会把 `goctl` 读作 `go-C-T-L`,这种是错误的念法,应参照 `go control` 读做 `ɡō kənˈtrōl`
## 查看版本信息
```shell
$ goctl -v
```
如果安装了goctl则会输出以下格式的文本信息
```text
goctl version ${version} ${os}/${arch}
```
例如输出:
```text
goctl version 1.1.5 darwin/amd64
```
版本号说明
* versiongoctl 版本号
* os当前操作系统名称
* arch 当前系统架构名称
## 安装 goctl
### 方式一go get
```shell
$ GO111MODULE=on GOPROXY=https://goproxy.cn/,direct go get -u github.com/tal-tech/go-zero/tools/goctl
```
通过此命令可以将goctl工具安装到 `$GOPATH/bin` 目录下
### 方式二 fork and build
从 go-zero代码仓库 `git@github.com:tal-tech/go-zero.git` 拉取一份源码,进入 `tools/goctl/`目录下编译一下 goctl 文件,然后将其添加到环境变量中。
安装完成后执行`goctl -v`,如果输出版本信息则代表安装成功,例如:
```shell
$ goctl -v
goctl version 1.1.4 darwin/amd64
```
## 常见问题
```
command not found: goctl
```
请确保goctl已经安装或者goctl是否已经正确添加到当前shell的环境变量中。

View File

@ -0,0 +1,53 @@
# Golang环境安装
## 前言
开发golang程序必然少不了对其环境的安装我们这里选择以1.15.1为例。
## 官方文档
[https://golang.google.cn/doc/install](https://golang.google.cn/doc/install)
## mac OS安装Go
* 下载并安装[Go for Mac](https://dl.google.com/go/go1.15.1.darwin-amd64.pkg)
* 验证安装结果
```shell
$ go version
```
```text
go version go1.15.1 darwin/amd64
```
## linux 安装Go
* 下载[Go for Linux](https://golang.org/dl/go1.15.8.linux-amd64.tar.gz)
* 解压压缩包至`/usr/local`
```shell
$ tar -C /usr/local -xzf go1.15.8.linux-amd64.tar.gz
```
* 添加`/usr/local/go/bin`到环境变量
```shell
$ $HOME/.profile
```
```shell
export PATH=$PATH:/usr/local/go/bin
```
```shell
$ source $HOME/.profile
```
* 验证安装结果
```shell
$ go version
```
```text
go version go1.15.1 linux/amd64
```
## Windows安装Go
* 下载并安装[Go for Windows](https://golang.org/dl/go1.15.8.windows-amd64.msi)
* 验证安装结果
```shell
$ go version
```
```text
go version go1.15.1 windows/amd64
```
## 其他
更多操作系统安装见[https://golang.org/dl/](https://golang.org/dl/)

View File

@ -0,0 +1,37 @@
# Go Module设置
## Go Module介绍
> Modules are how Go manages dependencies.[1]
即Go Module是Golang管理依赖性的方式像Java中的MavenAndroid中的Gradle类似。
## MODULE配置
* 查看`GO111MODULE`开启情况
```shell
$ go env GO111MODULE
```
```text
on
```
* 开启`GO111MODULE`,如果已开启(即执行`go env GO111MODULE`结果为`on`)请跳过。
```shell
$ go env -w GO111MODULE="on"
```
* 设置GOPROXY
```shell
$ go env -w GOPROXY=https://goproxy.cn
```
* 设置GOMODCACHE
查看GOMODCACHE
```shell
$ go env GOMODCACHE
```
如果目录不为空或者`/dev/null`,请跳过。
```shell
go env -w GOMODCACHE=$GOPATH/pkg/mod
```
# 参考文档
[1] [Go Modules Reference](https://golang.google.cn/ref/mod)

View File

@ -0,0 +1,7 @@
# Go夜读
* [2020-08-16 晓黑板 go-zero 微服务框架的架构设计](https://talkgo.org/t/topic/729)
* [2020-10-03 go-zero 微服务框架和线上交流](https://talkgo.org/t/topic/1070)
* [防止缓存击穿之进程内共享调用](https://talkgo.org/t/topic/968)
* [基于go-zero实现JWT认证](https://talkgo.org/t/topic/1114)
* [再见go-micro企业项目迁移go-zero全攻略](https://talkgo.org/t/topic/1607)

3
go-zero.dev/cn/gotalk.md Normal file
View File

@ -0,0 +1,3 @@
# Go开源说
* [Go 开源说第四期 - Go-Zero](https://www.bilibili.com/video/BV1Jy4y127Xu)

114
go-zero.dev/cn/intellij.md Normal file
View File

@ -0,0 +1,114 @@
# intellij插件
## Go-Zero Plugin
[<img src="https://img.shields.io/badge/Github-go--zero-brightgreen?logo=github" alt="go-zero"/>](https://github.com/tal-tech/go-zero)
[<img src="https://img.shields.io/badge/License-MIT-blue" alt="license"/>](https://github.com/zeromicro/goctl-intellij/blob/main/LICENSE)
[<img src="https://img.shields.io/badge/Release-0.7.14-red" alt="release"/>](https://github.com/zeromicro/goctl-intellij/releases)
[<img src="https://github.com/zeromicro/goctl-intellij/workflows/Java%20CI%20with%20Gradle/badge.svg" alt="Java CI with Gradle" />](https://github.com/zeromicro/goctl-intellij/actions)
## 介绍
一款支持go-zero api语言结构语法高亮、检测以及api、rpc、model快捷生成的插件工具。
## idea版本要求
* IntelliJ 2019.3+ (Ultimate or Community)
* Goland 2019.3+
* WebStorm 2019.3+
* PhpStorm 2019.3+
* PyCharm 2019.3+
* RubyMine 2019.3+
* CLion 2019.3+
## 版本特性
* api语法高亮
* api语法、语义检测
* struct、route、handler重复定义检测
* type跳转到类型声明位置
* 上下文菜单中支持api、rpc、mode相关menu选项
* 代码格式化(option+command+L)
* 代码提示
## 安装方式
### 方式一
在github的release中找到最新的zip包下载本地安装即可。无需解压
### 方式二
在plugin商店中搜索`Goctl`安装即可
## 预览
![preview](./resource/api-compare.png)
## 新建 Api(Proto) file
在工程区域目标文件夹`右键->New-> New Api(Proto) File ->Empty File/Api(Proto) Template`,如图:
![preview](./resource/api-new.png)
# 快速生成api/rpc服务
在目标文件夹`右键->New->Go Zero -> Api Greet Service/Rpc Greet Service`
![preview](./resource/service.png)
# Api/Rpc/Model Code生成
## 方法一(工程区域)
对应文件api、proto、sql`右键->New->Go Zero-> Api/Rpc/Model Code`,如图:
![preview](./resource/project_generate_code.png)
## 方法二(编辑区域)
对应文件api、proto、sql`右键-> Generate-> Api/Rpc/Model Code`
# 错误提示
![context menu](./resource/alert.png)
# Live Template
Live Template可以加快我们对api文件的编写比如我们在go文件中输入`main`关键字根据tip回车后会插入一段模板代码
```go
func main(){
}
```
或者说看到下图你会更加熟悉曾几何时你还在这里定义过template
![context menu](./resource/go_live_template.png)
下面就进入今天api语法中的模板使用说明吧我们先来看看service模板的效果
![context menu](./resource/live_template.gif)
首先上一张图了解一下api文件中几个模板生效区域psiTree元素区域
![context menu](./resource/psiTree.png)
#### 预设模板及生效区域
| 模板关键字 | psiTree生效区域 |描述
| ---- | ---- | ---- |
| @doc | ApiService |doc注释模板|
| doc | ApiService |doc注释模板|
| struct | Struct |struct声明模板|
| info | ApiFile |info block模板|
| type | ApiFile |type group模板|
| handler | ApiService |handler文件名模板|
| get | ApiService |get方法路由模板|
| head | ApiService |head方法路由模板|
| post | ApiService |post方法路由模板|
| put | ApiService |put方法路由模板|
| delete | ApiService |delete方法路由模板|
| connect | ApiService |connect方法路由模板|
| options | ApiService |options方法路由模板|
| trace | ApiService |trace方法路由模板|
| service | ApiFile |service服务block模板|
| json | Tag、Tag literal |tag模板|
| xml | Tag、Tag literal |tag模板|
| path | Tag、Tag literal |tag模板|
| form | Tag、Tag literal |tag模板|
关于每个模板对应内容可在`Goland(mac Os)->Preference->Editor->Live Templates-> Api|Api Tags`中查看详细模板内容如json tag模板内容为
```go
json:"$FIELD_NAME$"
```
![context menu](./resource/json_tag.png)

53
go-zero.dev/cn/join-us.md Normal file
View File

@ -0,0 +1,53 @@
# 加入我们
## 概要
<img src="./resource/go-zero-logo.png" alt="go-zero" width="100px" height="100px" align="right" />
[go-zero](https://github.com/tal-tech/go-zero) 是一个基于[MIT License](https://github.com/tal-tech/go-zero/blob/master/LICENSE) 的开源项目大家在使用中发现bug有新的特性等均可以参与到go-zero的贡献中来我们非常欢迎大家的积极参与也会最快响应大家提出的各种问题pr等。
## 贡献形式
* [Pull Request](https://github.com/tal-tech/go-zero/pulls)
* [Issue](https://github.com/tal-tech/go-zero/issues)
## 贡献须知
go-zero 的Pull request中的代码需要满足一定规范
* 命名规范,请阅读[命名规范](naming-spec.md)
* 以英文注释为主
* pr时备注好功能特性描述需要清晰简洁
* 增加单元测试覆盖率达80%+
## 贡献代码pr
* 进入[go-zero](https://github.com/tal-tech/go-zero) 项目fork一份[go-zero](https://github.com/tal-tech/go-zero) 项目到自己的github仓库中。
* 回到自己的github主页找到`xx/go-zero`项目其中xx为你的用户名如`anqiansong/go-zero`
![fork](./resource/fork.png)
* 克隆代码到本地
![clone](./resource/clone.png)
* 开发代码push到自己的github仓库
* 进入自己的github中go-zero项目点击浮层上的的`【Pull requests】`进入Compare页面。
![pr](./resource/new_pr.png)
* `base repository`选择`tal-tech/go-zero` `base:master`,`head repository`选择`xx/go-zero` `compare:$branch` `$branch`为你开发的分支,如图:
![pr](./resource/compare.png)
* 点击`【Create pull request】`即可实现pr申请
* 确认pr是否提交成功进入[go-zero](https://github.com/tal-tech/go-zero) 的[Pull requests](https://github.com/tal-tech/go-zero/pulls) 查看,应该有自己提交的记录,名称为你的开发时的分支名称
![pr record](./resource/pr_record.png)
## Issue
在我们的社区中有很多伙伴会积极的反馈一些go-zero使用过程中遇到的问题由于社区人数较多我们虽然会实时的关注社区动态但大家问题反馈过来都是随机的当我们团队还在解决某一个伙伴提出的问题时另外的问题也反馈上来可能会导致团队会很容易忽略掉为了能够一一的解决大家的问题我们强烈建议大家通过issue的方式来反馈问题包括但不限于bug期望的新功能特性等我们在实现某一个新特性时也会在issue中体现大家在这里也能够在这里获取到go-zero的最新动向也欢迎大家来积极的参与讨论。
### 怎么提Issue
* 点击[这里](https://github.com/tal-tech/go-zero/issues) 进入go-zero的Issue页面或者直接访问[https://github.com/tal-tech/go-zero/issues](https://github.com/tal-tech/go-zero/issues) 地址
* 点击右上角的`【New issue】`新建issue
* 填写issue标题和内容
* 点击`【Submit new issue】`提交issue
## 参考文档
* [Github Pull request](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/proposing-changes-to-your-work-with-pull-requests)

238
go-zero.dev/cn/jwt.md Normal file
View File

@ -0,0 +1,238 @@
# jwt鉴权
## 概述
> JSON Web令牌JWT是一个开放标准RFC 7519它定义了一种紧凑而独立的方法用于在各方之间安全地将信息作为JSON对象传输。由于此信息是经过数字签名的因此可以被验证和信任。可以使用秘密使用HMAC算法或使用RSA或ECDSA的公钥/私钥对对JWT进行签名。
## 什么时候应该使用JWT
* 授权这是使用JWT的最常见方案。一旦用户登录每个后续请求将包括JWT从而允许用户访问该令牌允许的路由服务和资源。单一登录是当今广泛使用JWT的一项功能因为它的开销很小并且可以在不同的域中轻松使用。
* 信息交换JSON Web令牌是在各方之间安全地传输信息的一种好方法。因为可以对JWT进行签名例如使用公钥/私钥对),所以您可以确保发件人是他们所说的人。此外,由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否未被篡改。
## 为什么要使用JSON Web令牌
由于JSON不如XML冗长因此在编码时JSON的大小也较小从而使JWT比SAML更为紧凑。这使得JWT是在HTML和HTTP环境中传递的不错的选择。
在安全方面只能使用HMAC算法由共享机密对SWT进行对称签名。但是JWT和SAML令牌可以使用X.509证书形式的公用/专用密钥对进行签名。与签署JSON的简单性相比使用XML Digital Signature签署XML而不引入模糊的安全漏洞是非常困难的。
JSON解析器在大多数编程语言中都很常见因为它们直接映射到对象。相反XML没有自然的文档到对象的映射。与SAML断言相比这使使用JWT更加容易。
关于用法JWT是在Internet规模上使用的。这突显了在多个平台尤其是移动平台上对JSON Web令牌进行客户端处理的简便性。
> [!TIP]
> 以上内容全部来自[jwt官网介绍](https://jwt.io/introduction)
## go-zero中怎么使用jwt
jwt鉴权一般在api层使用我们这次演示工程中分别在user api登录时生成jwt token在search api查询图书时验证用户jwt token两步来实现。
### user api生成jwt token
接着[业务编码](business-coding.md)章节的内容,我们完善上一节遗留的`getJwtToken`方法即生成jwt token逻辑
#### 添加配置定义和yaml配置项
```shell
$ vim service/user/cmd/api/internal/config/config.go
```
```go
type Config struct {
rest.RestConf
Mysql struct{
DataSource string
}
CacheRedis cache.CacheConf
Auth struct {
AccessSecret string
AccessExpire int64
}
}
```
```shell
$ vim service/user/cmd/api/etc/user-api.yaml
```
```yaml
Name: user-api
Host: 0.0.0.0
Port: 8888
Mysql:
DataSource: $user:$password@tcp($url)/$db?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai
CacheRedis:
- Host: $host
Pass: $pass
Type: node
Auth:
AccessSecret: $AccessSecret
AccessExpire: $AccessExpire
```
> [!TIP]
> $AccessSecret生成jwt token的密钥最简单的方式可以使用一个uuid值。
>
> $AccessExpirejwt token有效期单位
>
> 更多配置信息,请参考[api配置介绍](api-config.md)
```shell
$ vim service/user/cmd/api/internal/logic/loginlogic.go
```
```go
func (l *LoginLogic) getJwtToken(secretKey string, iat, seconds, userId int64) (string, error) {
claims := make(jwt.MapClaims)
claims["exp"] = iat + seconds
claims["iat"] = iat
claims["userId"] = userId
token := jwt.New(jwt.SigningMethodHS256)
token.Claims = claims
return token.SignedString([]byte(secretKey))
}
```
### search api使用jwt token鉴权
#### 编写search.api文件
```shell
$ vim service/search/cmd/api/search.api
```
```text
type (
SearchReq {
// 图书名称
Name string `form:"name"`
}
SearchReply {
Name string `json:"name"`
Count int `json:"count"`
}
)
@server(
jwt: Auth
)
service search-api {
@handler search
get /search/do (SearchReq) returns (SearchReply)
}
service search-api {
@handler ping
get /search/ping
}
```
> [!TIP]
> `jwt: Auth`开启jwt鉴权
>
> 如果路由需要jwt鉴权则需要在service上方声明此语法标志如上文中的` /search/do`
>
> 不需要jwt鉴权的路由就无需声明如上文中`/search/ping`
>
> 更多语法请阅读[api语法介绍](api-grammar.md)
#### 生成代码
前面已经描述过有三种方式去生成代码,这里就不赘述了。
#### 添加yaml配置项
```shell
$ vim service/search/cmd/api/etc/search-api.yaml
```
```yaml
Name: search-api
Host: 0.0.0.0
Port: 8889
Auth:
AccessSecret: $AccessSecret
AccessExpire: $AccessExpire
```
> [!TIP]
> $AccessSecret这个值必须要和user api中声明的一致。
>
> $AccessExpire: 有效期
>
> 这里修改一下端口避免和user api端口8888冲突
### 验证 jwt token
* 启动user api服务登录
```shell
$ cd service/user/cmd/api
$ go run user.go -f etc/user-api.yaml
```
```text
Starting server at 0.0.0.0:8888...
```
```shell
$ curl -i -X POST \
http://127.0.0.1:8888/user/login \
-H 'content-type: application/json' \
-d '{
"username":"666",
"password":"123456"
}'
```
```text
HTTP/1.1 200 OK
Content-Type: application/json
Date: Mon, 08 Feb 2021 10:37:54 GMT
Content-Length: 251
{"id":1,"name":"小明","gender":"男","accessToken":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MTI4NjcwNzQsImlhdCI6MTYxMjc4MDY3NCwidXNlcklkIjoxfQ.JKa83g9BlEW84IiCXFGwP2aSd0xF3tMnxrOzVebbt80","accessExpire":1612867074,"refreshAfter":1612823874}
```
* 启动search api服务调用`/search/do`验证jwt鉴权是否通过
```shell
$ go run search.go -f etc/search-api.yaml
```
```text
Starting server at 0.0.0.0:8889...
```
我们先不传jwt token看看结果
```shell
$ curl -i -X GET \
'http://127.0.0.1:8889/search/do?name=%E8%A5%BF%E6%B8%B8%E8%AE%B0'
```
```text
HTTP/1.1 401 Unauthorized
Date: Mon, 08 Feb 2021 10:41:57 GMT
Content-Length: 0
```
很明显jwt鉴权失败了返回401的statusCode接下来我们带一下jwt token即用户登录返回的`accessToken`
```shell
$ curl -i -X GET \
'http://127.0.0.1:8889/search/do?name=%E8%A5%BF%E6%B8%B8%E8%AE%B0' \
-H 'authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MTI4NjcwNzQsImlhdCI6MTYxMjc4MDY3NCwidXNlcklkIjoxfQ.JKa83g9BlEW84IiCXFGwP2aSd0xF3tMnxrOzVebbt80'
```
```text
HTTP/1.1 200 OK
Content-Type: application/json
Date: Mon, 08 Feb 2021 10:44:45 GMT
Content-Length: 21
{"name":"","count":0}
```
> [!TIP]
> 服务启动错误,请查看[常见错误处理](error.md)
至此jwt从生成到使用就演示完成了jwt token的鉴权是go-zero内部已经封装了你只需在api文件中定义服务时简单的声明一下即可。
### 获取jwt token中携带的信息
go-zero从jwt token解析后会将用户生成token时传入的kv原封不动的放在http.Request的Context中因此我们可以通过Context就可以拿到你想要的值
```shell
$ vim /service/search/cmd/api/internal/logic/searchlogic.go
```
添加一个log来输出从jwt解析出来的userId。
```go
func (l *SearchLogic) Search(req types.SearchReq) (*types.SearchReply, error) {
logx.Infof("userId: %v",l.ctx.Value("userId"))// 这里的key和生成jwt token时传入的key一致
return &types.SearchReply{}, nil
}
```
运行结果
```text
{"@timestamp":"2021-02-09T10:29:09.399+08","level":"info","content":"userId: 1"}
```
# 猜你想看
* [jwt介绍](https://jwt.io/)
* [api配置介绍](api-config.md)
* [api语法](api-grammar.md)

View File

@ -0,0 +1,5 @@
# 学习资源
这里将不定期更新go-zero的最新学习资源通道目前包含通道有
* [公众号](wechat.md)
* [Go夜读](goreading.md)
* [Go开源说](gotalk.md)

View File

@ -0,0 +1,139 @@
# 日志收集
为了保证业务稳定运行,预测服务不健康风险,日志的收集可以帮助我们很好的观察当前服务的健康状况,
在传统业务开发中,机器部署还不是很多时,我们一般都是直接登录服务器进行日志查看、调试,但随着业务的增大,服务的不断拆分,
服务的维护成本也会随之变得越来越复杂,在分布式系统中,服务器机子增多,服务分布在不同的服务器上,当遇到问题时,
我们不能使用传统做法,登录到服务器进行日志排查和调试,这个复杂度可想而知。
![log-flow](./resource/log-flow.png)
> [!TIP]
> 如果是一个简单的单体服务系统或者服务过于小不建议直接使用,否则会适得其反。
## 准备工作
* kafka
* elasticsearch
* kibana
* filebeat、Log-Pilotk8s
* go-stash
## filebeat配置
```shell
$ vim xx/filebeat.yaml
```
```yaml
filebeat.inputs:
- type: log
enabled: true
# 开启json解析
json.keys_under_root: true
json.add_error_key: true
# 日志文件路径
paths:
- /var/log/order/*.log
setup.template.settings:
index.number_of_shards: 1
# 定义kafka topic field
fields:
log_topic: log-collection
# 输出到kafka
output.kafka:
hosts: ["127.0.0.1:9092"]
topic: '%{[fields.log_topic]}'
partition.round_robin:
reachable_only: false
required_acks: 1
keep_alive: 10s
# ================================= Processors =================================
processors:
- decode_json_fields:
fields: ['@timestamp','level','content','trace','span','duration']
target: ""
```
> [!TIP]
> xx为filebeat.yaml所在路径
## go-stash配置
* 新建`config.yaml`文件
* 添加配置内容
```shell
$ vim config.yaml
```
```yaml
Clusters:
- Input:
Kafka:
Name: go-stash
Log:
Mode: file
Brokers:
- "127.0.0.1:9092"
Topics:
- log-collection
Group: stash
Conns: 3
Consumers: 10
Processors: 60
MinBytes: 1048576
MaxBytes: 10485760
Offset: first
Filters:
- Action: drop
Conditions:
- Key: status
Value: "503"
Type: contains
- Key: type
Value: "app"
Type: match
Op: and
- Action: remove_field
Fields:
- source
- _score
- "@metadata"
- agent
- ecs
- input
- log
- fields
Output:
ElasticSearch:
Hosts:
- "http://127.0.0.1:9200"
Index: "go-stash-{{yyyy.MM.dd}}"
MaxChunkBytes: 5242880
GracePeriod: 10s
Compress: false
TimeZone: UTC
```
## 启动服务(按顺序启动)
* 启动kafka
* 启动elasticsearch
* 启动kibana
* 启动go-stash
* 启动filebeat
* 启动order-api服务及其依赖服务go-zero-demo工程中的order-api服务
## 访问kibana
进入127.0.0.1:5601
![log](./resource/log.png)
> [!TIP]
> 这里仅演示收集服务中通过logx产生的日志nginx中日志收集同理。
# 参考文档
* [kafka](http://kafka.apache.org/)
* [elasticsearch](https://www.elastic.co/cn/elasticsearch/)
* [kibana](https://www.elastic.co/cn/kibana)
* [filebeat](https://www.elastic.co/cn/beats/filebeat)
* [go-stash](https://github.com/tal-tech/go-stash)
* [filebeat配置](https://www.elastic.co/guide/en/beats/filebeat/current/index.html)

185
go-zero.dev/cn/logx.md Normal file
View File

@ -0,0 +1,185 @@
# logx
## 使用示例
```go
var c logx.LogConf
// 从 yaml 文件中 初始化配置
conf.MustLoad("config.yaml", &c)
// logx 根据配置初始化
logx.MustSetup(c)
logx.Info("This is info!")
logx.Infof("This is %s!", "info")
logx.Error("This is error!")
logx.Errorf("this is %s!", "error")
logx.Close()
```
## 初始化
logx 有很多可以配置项,可以参考 logx.LogConf 中的定义。目前可以使用
```go
logx.MustSetUp(c)
```
进行初始化配置,如果没有进行初始化配置,所有的配置将使用默认配置。
## Level
logx 支持的打印日志级别有:
- info
- error
- server
- fatal
- slow
- stat
可以使用对应的方法打印出对应级别的日志。
同时为了方便调试,线上使用,可以动态调整日志打印级别,其中可以通过 **logx.SetLevel(uint32)** 进行级别设置,也可以通过配置初始化进行设置。目前支持的参数为:
```go
const (
// 打印所有级别的日志
InfoLevel = iotas
// 打印 errors, slows, stacks 日志
ErrorLevel
// 仅打印 server 级别日志
SevereLevel
)
```
## 日志模式
目前日志打印模式主要分为2种一种文件输出一种控制台输出。推荐方式当采用 k8sdocker 等部署方式的时候,可以将日志输出到控制台,使用日志收集器收集导入至 es 进行日志分析。如果是直接部署方式可以采用文件输出方式logx 会自动在指定文件目录创建对应 5 个对应级别的的日志文件保存日志。
```bash
.
├── access.log
├── error.log
├── severe.log
├── slow.log
└── stat.log
```
同时会按照自然日进行文件分割,当超过指定配置天数,会对日志文件进行自动删除,打包等操作。
## 禁用日志
如果不需要日志打印,可以使用 **logx.Close()** 关闭日志输出。注意,当禁用日志输出,将无法在次打开,具体可以参考 **logx.RotateLogger****logx.DailyRotateRule** 的实现。
## 关闭日志
因为 logx 采用异步进行日志输出,如果没有正常关闭日志,可能会造成部分日志丢失的情况。必须在程序退出的地方关闭日志输出:
```go
logx.Close()
```
框架中 rest 和 zrpc 等大部分地方已经做好了日志配置和关闭相关操作,用户可以不用关心。
同时注意,当关闭日志输出之后,将无法在次打印日志了。
推荐写法:
```go
import "github.com/tal-tech/go-zero/core/proc"
// grace close log
proc.AddShutdownListener(func() {
logx.Close()
})
```
## Duration
我们打印日志的时候可能需要打印耗时情况,可以使用 **logx.WithDuration(time.Duration)**, 参考如下示例:
```go
startTime := timex.Now()
// 数据库查询
rows, err := conn.Query(q, args...)
duration := timex.Since(startTime)
if duration > slowThreshold {
logx.WithDuration(duration).Slowf("[SQL] query: slowcall - %s", stmt)
} else {
logx.WithDuration(duration).Infof("sql query: %s", stmt)
}
```
会输出如下格式
```json
{"@timestamp":"2020-09-12T01:22:55.552+08","level":"info","duration":"3.0ms","content":"sql query:..."}
{"@timestamp":"2020-09-12T01:22:55.552+08","level":"slow","duration":"500ms","content":"[SQL] query: slowcall - ..."}
```
这样就可以很容易统计出慢 sql 相关信息。
## TraceLog
tracingEntry 是为了链路追踪日志输出定制的。可以打印 context 中的 traceId 和 spanId 信息,配合我们的 **rest****zrpc** 很容易完成链路日志的相关打印。示例如下
```go
logx.WithContext(context.Context).Info("This is info!")
```
## SysLog
应用中可能有部分采用系统 log 进行日志打印logx 同样封装方法,很容易将 log 相关的日志收集到 logx 中来。
```go
logx.CollectSysLog()
```
# 日志配置相关
**LogConf** 定义日志系统所需的基本配置
完整定义如下:
```go
type LogConf struct {
ServiceName string `json:",optional"`
Mode string `json:",default=console,options=console|file|volume"`
Path string `json:",default=logs"`
Level string `json:",default=info,options=info|error|severe"`
Compress bool `json:",optional"`
KeepDays int `json:",optional"`
StackCooldownMillis int `json:",default=100"`
}
```
## Mode
**Mode** 定义了日志打印的方式。默认的模式是 **console** 打印到控制台上面。
目前支持的模式如下:
- console
- 打印到控制台
- file
- 打印到指定路径下的access.log, error.log, stat.log等文件里
- volume
- 为了在k8s内打印到mount进来的存储上因为多个pod可能会覆盖相同的文件volume模式自动识别pod并按照pod分开写各自的日志文件
## Path
**Path** 定义了文件日志的输出路径,默认值为 **logs**
## Level
**Level** 定义了日志打印级别,默认值为 **info**
目前支持的级别如下:
- info
- error
- severe
## Compress
**Compress** 定义了日志是否需要压缩,默认值为 **false**。在 Mode 为 file 模式下面,文件最后会进行打包压缩成 .gz 文件。
## KeepDays
**KeepDays** 定义日志最大保留天数,默认值为 0表示不会删除旧的日志。在 Mode 为 file 模式下面,如果超过了最大保留天数,旧的日志文件将会被删除。
## StackCooldownMillis
**StackCooldownMillis** 定义了日志输出间隔,默认为 100 毫秒。

View File

@ -0,0 +1,309 @@
# 微服务
在上一篇我们已经演示了怎样快速创建一个单体服务,接下来我们来演示一下如何快速创建微服务,
在本小节中api部分其实和单体服务的创建逻辑是一样的只是在单体服务中没有服务间的通讯而已
且微服务中api服务会多一些rpc调用的配置。
## 前言
本小节将以一个`订单服务`调用`用户服务`来简单演示一下,演示代码仅传递思路,其中有些环节不会一一列举。
## 情景提要
假设我们在开发一个商城项目,而开发者小明负责用户模块(user)和订单模块(order)的开发,我们姑且将这两个模块拆分成两个微服务①
> [!NOTE]
> ①:微服务的拆分也是一门学问,这里我们就不讨论怎么去拆分微服务的细节了。
## 演示功能目标
* 订单服务(order)提供一个查询接口
* 用户服务(user)提供一个方法供订单服务获取用户信息
## 服务设计分析
根据情景提要我们可以得知订单是直接面向用户通过http协议访问数据而订单内部需要获取用户的一些基础数据既然我们的服务是采用微服务的架构设计
那么两个服务user,order就必须要进行数据交换服务间的数据交换即服务间的通讯到了这里采用合理的通讯协议也是一个开发人员需要
考虑的事情可以通过httprpc等方式来进行通讯这里我们选择rpc来实现服务间的通讯相信这里我已经对"rpc服务存在有什么作用"已经作了一个比较好的场景描述。
当然,一个服务开发前远不止这点设计分析,我们这里就不详细描述了。从上文得知,我们需要一个
* user rpc
* order api
两个服务来初步实现这个小demo。
## 创建mall工程
```shell
$ cd ~/go-zero-demo
$ mkdir mall && cd mall
```
## 创建user rpc服务
* 创建user rpc服务
```shell
$ cd ~/go-zero-demo/mall
$ mkdir -p user/rpc && cd user/rpc
```
* 添加`user.proto`文件,增加`getUser`方法
```shell
$ vim ~/go-zero-demo/mall/user/rpc/user.proto
```
```protobuf
syntax = "proto3";
package user;
option go_package = "user";
message IdRequest {
string id = 1;
}
message UserResponse {
// 用户id
string id = 1;
// 用户名称
string name = 2;
// 用户性别
string gender = 3;
}
service User {
rpc getUser(IdRequest) returns(UserResponse);
}
```
* 生成代码
```shell
$ cd ~/go-zero-demo/mall/user/rpc
$ goctl rpc proto -src user.proto -dir .
[goclt version <=1.2.1] protoc -I=/Users/xx/mall/user user.proto --goctl_out=plugins=grpc:/Users/xx/mall/user/user
[goctl version > 1.2.1] protoc -I=/Users/xx/mall/user user.proto --go_out=plugins=grpc:/Users/xx/mall/user/user
Done.
```
> [!TIPS]
> 如果安装的 `protoc-gen-go` 版大于1.4.0, proto文件建议加上`go_package`
* 填充业务逻辑
```shell
$ vim internal/logic/getuserlogic.go
```
```go
package logic
import (
"context"
"go-zero-demo/mall/user/rpc/internal/svc"
"go-zero-demo/mall/user/rpc/user"
"github.com/tal-tech/go-zero/core/logx"
)
type GetUserLogic struct {
ctx context.Context
svcCtx *svc.ServiceContext
logx.Logger
}
func NewGetUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserLogic {
return &GetUserLogic{
ctx: ctx,
svcCtx: svcCtx,
Logger: logx.WithContext(ctx),
}
}
func (l *GetUserLogic) GetUser(in *user.IdRequest) (*user.UserResponse, error) {
return &user.UserResponse{
Id: "1",
Name: "test",
}, nil
}
```
* 修改配置
```shell
$ vim internal/config/config.go
```
```go
package config
import (
"github.com/tal-tech/go-zero/zrpc"
)
type Config struct {
zrpc.RpcServerConf
}
```
## 创建order api服务
* 创建 `order api`服务
```shell
$ cd ~/go-zero-demo/mall
$ mkdir -p order/api && cd order/api
```
* 添加api文件
```shell
$ vim order.api
```
```go
type(
OrderReq {
Id string `path:"id"`
}
OrderReply {
Id string `json:"id"`
Name string `json:"name"`
}
)
service order {
@handler getOrder
get /api/order/get/:id (OrderReq) returns (OrderReply)
}
```
* 生成order服务
```shell
$ goctl api go -api order.api -dir .
Done.
```
* 添加user rpc配置
```shell
$ vim internal/config/config.go
```
```go
package config
import "github.com/tal-tech/go-zero/rest"
import "github.com/tal-tech/go-zero/zrpc"
type Config struct {
rest.RestConf
UserRpc zrpc.RpcClientConf
}
```
* 添加yaml配置
```shell
$ vim etc/order.yaml
```
```yaml
Name: order
Host: 0.0.0.0
Port: 8888
UserRpc:
Etcd:
Hosts:
- 127.0.0.1:2379
Key: user.rpc
```
* 完善服务依赖
```shell
$ vim internal/svc/servicecontext.go
```
```go
package svc
import (
"go-zero-demo/mall/order/api/internal/config"
"go-zero-demo/mall/user/rpc/userclient"
"github.com/tal-tech/go-zero/zrpc"
)
type ServiceContext struct {
Config config.Config
UserRpc userclient.User
}
func NewServiceContext(c config.Config) *ServiceContext {
return &ServiceContext{
Config: c,
UserRpc: userclient.NewUser(zrpc.MustNewClient(c.UserRpc)),
}
}
```
* 添加order演示逻辑
给`getorderlogic`添加业务逻辑
```shell
$ vim ~/go-zero-demo/mall/order/api/internal/logic/getorderlogic.go
```
```go
func (l *GetOrderLogic) GetOrder(req types.OrderReq) (*types.OrderReply, error) {
user, err := l.svcCtx.UserRpc.GetUser(l.ctx, &userclient.IdRequest{
Id: "1",
})
if err != nil {
return nil, err
}
if user.Name != "test" {
return nil, errors.New("用户不存在")
}
return &types.OrderReply{
Id: req.Id,
Name: "test order",
}, nil
}
```
## 启动服务并验证
* 启动etcd
```shell
$ etcd
```
* 启动user rpc
```shell
$ go run user.go -f etc/user.yaml
```
```text
Starting rpc server at 127.0.0.1:8080...
```
* 启动order api
```shell
$ go run order.go -f etc/order.yaml
```
```text
Starting server at 0.0.0.0:8888...
```
* 访问order api
```shell
curl -i -X GET \
http://localhost:8888/api/order/get/1
```
```text
HTTP/1.1 200 OK
Content-Type: application/json
Date: Sun, 07 Feb 2021 03:45:05 GMT
Content-Length: 30
{"id":"1","name":"test order"}
```
> [!TIP]
> 在演示中的提及的api语法rpc生成goctlgoctl环境等怎么使用和安装快速入门中不作详细概述我们后续都会有详细的文档进行描述你也可以点击下文的【猜你想看】快速跳转的对应文档查看。
# 源码
[mall源码](https://github.com/zeromicro/go-zero-demo/tree/master/mall)
# 猜你想看
* [goctl使用说明](goctl.md)
* [api目录结构介绍](api-dir.md)
* [api语法](api-grammar.md)
* [api配置文件介绍](api-config.md)
* [api中间件使用](middleware.md)
* [rpc目录](rpc-dir.md)
* [rpc配置](rpc-config.md)
* [rpc调用方说明](rpc-call.md)

View File

@ -0,0 +1,124 @@
# 中间件使用
在上一节我们演示了怎么使用jwt鉴权相信你已经掌握了对jwt的基本使用本节我们来看一下api服务中间件怎么使用。
## 中间件分类
在go-zero中中间件可以分为路由中间件和全局中间件路由中间件是指某一些特定路由需要实现中间件逻辑其和jwt类似没有放在`jwt:xxx`下的路由不会使用中间件功能,
而全局中间件的服务范围则是整个服务。
## 中间件使用
这里以`search`服务为例来演示中间件的使用
### 路由中间件
* 重新编写`search.api`文件,添加`middleware`声明
```shell
$ cd service/search/cmd/api
$ vim search.api
```
```text
type SearchReq struct {}
type SearchReply struct {}
@server(
jwt: Auth
middleware: Example // 路由中间件声明
)
service search-api {
@handler search
get /search/do (SearchReq) returns (SearchReply)
}
```
* 重新生成api代码
```shell
$ goctl api go -api search.api -dir .
```
```text
etc/search-api.yaml exists, ignored generation
internal/config/config.go exists, ignored generation
search.go exists, ignored generation
internal/svc/servicecontext.go exists, ignored generation
internal/handler/searchhandler.go exists, ignored generation
internal/handler/pinghandler.go exists, ignored generation
internal/logic/searchlogic.go exists, ignored generation
internal/logic/pinglogic.go exists, ignored generation
Done.
```
生成完后会在`internal`目录下多一个`middleware`的目录,这里即中间件文件,后续中间件的实现逻辑也在这里编写。
* 完善资源依赖`ServiceContext`
```shell
$ vim service/search/cmd/api/internal/svc/servicecontext.go
```
```go
type ServiceContext struct {
Config config.Config
Example rest.Middleware
}
func NewServiceContext(c config.Config) *ServiceContext {
return &ServiceContext{
Config: c,
Example: middleware.NewExampleMiddleware().Handle,
}
}
```
* 编写中间件逻辑
这里仅添加一行日志内容example middle如果服务运行输出example middle则代表中间件使用起来了。
```shell
$ vim service/search/cmd/api/internal/middleware/examplemiddleware.go
```
```go
package middleware
import "net/http"
type ExampleMiddleware struct {
}
func NewExampleMiddleware() *ExampleMiddleware {
return &ExampleMiddleware{}
}
func (m *ExampleMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// TODO generate middleware implement function, delete after code implementation
// Passthrough to next handler if need
next(w, r)
}
}
```
* 启动服务验证
```text
{"@timestamp":"2021-02-09T11:32:57.931+08","level":"info","content":"example middle"}
```
### 全局中间件
通过rest.Server提供的Use方法即可
```go
func main() {
flag.Parse()
var c config.Config
conf.MustLoad(*configFile, &c)
ctx := svc.NewServiceContext(c)
server := rest.MustNewServer(c.RestConf)
defer server.Stop()
// 全局中间件
server.Use(func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
logx.Info("global middleware")
next(w, r)
}
})
handler.RegisterHandlers(server, ctx)
fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port)
server.Start()
}
```
```text
{"@timestamp":"2021-02-09T11:50:15.388+08","level":"info","content":"global middleware"}
```

View File

@ -0,0 +1,55 @@
# model生成
首先,下载好[演示工程](https://go-zero.dev/cn/resource/book.zip) 后我们以user的model来进行代码生成演示。
## 前言
model是服务访问持久化数据层的桥梁业务的持久化数据常存在于mysqlmongo等数据库中我们都知道对于一个数据库的操作莫过于CURD
而这些工作也会占用一部分时间来进行开发我曾经在编写一个业务时写了40个model文件根据不同业务需求的复杂性平均每个model文件差不多需要
10分钟对于40个文件来说400分钟的工作时间差不多一天的工作量而goctl工具可以在10秒钟来完成这400分钟的工作。
## 准备工作
进入演示工程book找到user/model下的user.sql文件将其在你自己的数据库中执行建表。
## 代码生成(带缓存)
### 方式一(ddl)
进入`service/user/model`目录,执行命令
```shell
$ cd service/user/model
$ goctl model mysql ddl -src user.sql -dir . -c
```
```text
Done.
```
### 方式二(datasource)
```shell
$ goctl model mysql datasource -url="$datasource" -table="user" -c -dir .
```
```text
Done.
```
> [!TIP]
> $datasource为数据库连接地址
### 方式三(intellij 插件)
在Goland中右键`user.sql`,依次进入并点击`New`->`Go Zero`->`Model Code`即可生成,或者打开`user.sql`文件,
进入编辑区,使用快捷键`Command+N`for mac OS或者 `alt+insert`for windows选择`Mode Code`即可
![model生成](https://zeromicro.github.io/go-zero-pages/resource/intellij-model.png)
> [!TIP]
> intellij插件生成需要安装goctl插件详情见[intellij插件](intellij.md)
## 验证生成的model文件
查看tree
```shell
$ tree
```
```text
.
├── user.sql
├── usermodel.go
└── vars.go
```
# 猜你想看
[model命令及其原理](goctl-model.md)

View File

@ -0,0 +1,90 @@
# 单体服务
## 前言
由于go-zero集成了web/rpc于一体社区有部分小伙伴会问我go-zero的定位是否是一款微服务框架
答案是否定的go-zero虽然集众多功能于一身但你可以将其中任何一个功能独立出来去单独使用也可以开发单体服务
不是说每个服务上来就一定要采用微服务的架构的设计,这点大家可以看看作者(kevin)的第四期[开源说](https://www.bilibili.com/video/BV1Jy4y127Xu) ,其中对此有详细的讲解。
## 创建greet服务
```shell
$ cd ~/go-zero-demo
$ goctl api new greet
Done.
```
查看一下`greet`服务的结构
```shell
$ cd greet
$ tree
```
```text
.
├── etc
│   └── greet-api.yaml
├── greet.api
├── greet.go
└── internal
├── config
│   └── config.go
├── handler
│   ├── greethandler.go
│   └── routes.go
├── logic
│   └── greetlogic.go
├── svc
│   └── servicecontext.go
└── types
└── types.go
```
由以上目录结构可以观察到,`greet`服务虽小,但"五脏俱全"。接下来我们就可以在`greetlogic.go`中编写业务代码了。
## 编写逻辑
```shell
$ vim ~/go-zero-demo/greet/internal/logic/greetlogic.go
```
```go
func (l *GreetLogic) Greet(req types.Request) (*types.Response, error) {
return &types.Response{
Message: "Hello go-zero",
}, nil
}
```
## 启动并访问服务
* 启动服务
```shell
$ cd ~/go-zero-demo/greet
$ go run greet.go -f etc/greet-api.yaml
```
```text
Starting server at 0.0.0.0:8888...
```
* 访问服务
```shell
$ curl -i -X GET \
http://localhost:8888/from/you
```
```text
HTTP/1.1 200 OK
Content-Type: application/json
Date: Sun, 07 Feb 2021 04:31:25 GMT
Content-Length: 27
{"message":"Hello go-zero"}
```
# 源码
[greet源码](https://github.com/zeromicro/go-zero-demo/tree/master/greet)
# 猜你想看
* [goctl使用说明](goctl.md)
* [api目录结构介绍](api-dir.md)
* [api语法](api-grammar.md)
* [api配置文件介绍](api-config.md)
* [api中间件使用](middleware.md)

180
go-zero.dev/cn/mysql.md Normal file
View File

@ -0,0 +1,180 @@
# mysql
`go-zero` 提供更易于操作的 `mysql` API。
> [!TIP]
> 但是 `stores/mysql` 定位不是一个 `orm` 框架,如果你需要通过 `sql/scheme` -> `model/struct` 逆向生成 `model` 层代码,可以使用「[goctl model](https://go-zero.dev/cn/goctl-model.html)」,这个是极好的功能。
## Feature
- 相比原生,提供对开发者更友好的 API
- 完成 `queryField -> struct` 的自动赋值
- 批量插入「bulkinserter」
- 自带熔断
- API 经过若干个服务的不断考验
- 提供 `partial assignment` 特性,不强制 `struct` 的严格赋值
## Connection
下面用一个例子简单说明一下如何创建一个 `mysql` 连接的 model
```go
// 1. 快速连接一个 mysql
// datasource: mysql dsn
heraMysql := sqlx.NewMysql(datasource)
// 2. 在 servicecontext 中调用懂model上层的logic层调用
model.NewMysqlModel(heraMysql, tablename),
// 3. model层 mysql operation
func NewMysqlModel(conn sqlx.SqlConn, table string) *MysqlModel {
defer func() {
recover()
}()
// 4. 创建一个批量insert的 [mysql executor]
// conn: mysql connection; insertsql: mysql insert sql
bulkInserter , err := sqlx.NewBulkInserter(conn, insertsql)
if err != nil {
logx.Error("Init bulkInsert Faild")
panic("Init bulkInsert Faild")
return nil
}
return &MysqlModel{conn: conn, table: table, Bulk: bulkInserter}
}
```
## CRUD
准备一个 `User model`
```go
var userBuilderQueryRows = strings.Join(builderx.FieldNames(&User{}), ",")
type User struct {
Avatar string `db:"avatar"` // 头像
UserName string `db:"user_name"` // 姓名
Sex int `db:"sex"` // 1男,2女
MobilePhone string `db:"mobile_phone"` // 手机号
}
```
其中 `userBuilderQueryRows`  `go-zero` 中提供 `struct -> [field...]` 的转化,开发者可以将此当成模版直接使用。
### insert
```go
// 一个实际的insert model层操作
func (um *UserModel) Insert(user *User) (int64, error) {
const insertsql = `insert into `+um.table+` (`+userBuilderQueryRows+`) values(?, ?, ?)`
// insert op
res, err := um.conn.Exec(insertsql, user.Avatar, user.UserName, user.Sex, user.MobilePhone)
if err != nil {
logx.Errorf("insert User Position Model Model err, err=%v", err)
return -1, err
}
id, err := res.LastInsertId()
if err != nil {
logx.Errorf("insert User Model to Id parse id err,err=%v", err)
return -1, err
}
return id, nil
}
```
- 拼接 `insertsql`
- 将 `insertsql` 以及占位符对应的 `struct field` 传入 -> `con.Exex(insertsql, field...)`
> [!WARNING]
> `conn.Exec(sql, args...)`  `args...` 需对应 `sql` 中的占位符。不然会出现赋值异常的问题。
`go-zero` 将涉及 `mysql` 修改的操作统一抽象为 `Exec()` 。所以 `insert/update/delete` 操作本质上是一致的。其余两个操作,开发者按照上述 `insert` 流程尝试即可。
### query
只需要传入 `querysql` 和 `model` 结构体,就可以获取到被赋值好的 `model` 。无需开发者手动赋值。
```go
func (um *UserModel) FindOne(uid int64) (*User, error) {
var user User
const querysql = `select `+userBuilderQueryRows+` from `+um.table+` where id=? limit 1`
err := um.conn.QueryRow(&user, querysql, uid)
if err != nil {
logx.Errorf("userId.findOne error, id=%d, err=%s", uid, err.Error())
if err == sqlx.ErrNotFound {
return nil, ErrNotFound
}
return nil, err
}
return &user, nil
}
```
- 声明 `model struct` ,拼接 `querysql`
- `conn.QueryRow(&model, querysql, args...)`  `args...` 与 `querysql` 中的占位符对应。
> [!WARNING]
> `QueryRow()` 中第一个参数需要传入 `Ptr` 「底层需要反射对 `struct` 进行赋值」
上述是查询一条记录,如果需要查询多条记录时,可以使用 `conn.QueryRows()`
```go
func (um *UserModel) FindOne(sex int) ([]*User, error) {
users := make([]*User, 0)
const querysql = `select `+userBuilderQueryRows+` from `+um.table+` where sex=?`
err := um.conn.QueryRows(&users, querysql, sex)
if err != nil {
logx.Errorf("usersSex.findOne error, sex=%d, err=%s", uid, err.Error())
if err == sqlx.ErrNotFound {
return nil, ErrNotFound
}
return nil, err
}
return users, nil
}
```
`QueryRow()` 不同的地方在于: `model` 需要设置成 `Slice` ,因为是查询多行,需要对多个 `model` 赋值。但同时需要注意️:第一个参数需要传入 `Ptr`
### querypartial
从使用上,与上述的 `QueryRow()` 无异「这正体现了 `go-zero` 高度的抽象设计」。
区别:
- `QueryRow()`  `len(querysql fields) == len(struct)` ,且一一对应
- `QueryRowPartial()` `len(querysql fields) <= len(struct)`
numA数据库字段数numB定义的 `struct` 属性数。
如果 `numA < numB` ,但是你恰恰又需要统一多处的查询时「定义了多个 `struct` 返回不同的用途,恰恰都可以使用相同的 `querysql` 」,就可以使用 `QueryRowPartial()`
## 事务
要在事务中执行一系列操作,一般流程如下:
```go
var insertsql = `insert into User(uid, username, mobilephone) values (?, ?, ?)`
err := usermodel.conn.Transact(func(session sqlx.Session) error {
stmt, err := session.Prepare(insertsql)
if err != nil {
return err
}
defer stmt.Close()
// 返回任何错误都会回滚事务
if _, err := stmt.Exec(uid, username, mobilephone); err != nil {
logx.Errorf("insert userinfo stmt exec: %s", err)
return err
}
// 还可以继续执行 insert/update/delete 相关操作
return nil
})
```
如同上述例子,开发者只需将 **事务** 中的操作都包装在一个函数 `func(session sqlx.Session) error {}` 中即可,如果事务中的操作返回任何错误, `Transact()` 都会自动回滚事务。

View File

@ -0,0 +1,49 @@
# 命名规范
在任何语言开发中,都有其语言领域的一些命名规范,好的命名可以:
* 降低代码阅读成本
* 降低维护难度
* 降低代码复杂度
## 规范建议
在我们实际开发中,有很多开发人可能是由某一语言转到另外一个语言领域,在转到另外一门语言后,
我们都会保留着对旧语言的编程习惯,在这里,我建议的是,虽然不同语言之前的某些规范可能是相通的,
但是我们最好能够按照官方的一些demo来熟悉是渐渐适应当前语言的编程规范而不是直接将原来语言的编程规范也随之迁移过来。
## 命名准则
* 当变量名称在定义和最后一次使用之间的距离很短时,简短的名称看起来会更好。
* 变量命名应尽量描述其内容,而不是类型
* 常量命名应尽量描述其值,而不是如何使用这个值
* 在遇到forif等循环或分支时推荐单个字母命名来标识参数和返回值
* method、interface、type、package推荐使用单词命名
* package名称也是命名的一部分请尽量将其利用起来
* 使用一致的命名风格
## 文件命名规范
* 全部小写
* 除unit test外避免下划线(_)
* 文件名称不宜过长
## 变量命名规范参考
* 首字母小写
* 驼峰命名
* 见名知义,避免拼音替代英文
* 不建议包含下划线(_)
* 不建议包含数字
**适用范围**
* 局部变量
* 函数出参、入参
## 函数、常量命名规范
* 驼峰式命名
* 可exported的必须首字母大写
* 不可exported的必须首字母小写
* 避免全部大写与下划线(_)组合
> [!TIP]
> 如果是go-zero代码贡献则必须严格遵循此命名规范
# 参考文档
* [Practical Go: Real world advice for writing maintainable Go programs](https://dave.cheney.net/practical-go/presentations/gophercon-singapore-2019.html#_simplicity)

View File

@ -0,0 +1,154 @@
# 10月3日线上交流问题汇总
- go-zero适用场景
- 希望说说应用场景,各个场景下的优势
- 高并发的微服务系统
- 支撑千万级日活百万级QPS
- 完整的微服务治理能力
- 支持自定义中间件
- 很好的管理了数据库和缓存
- 有效隔离故障
- 低并发的单体系统
- 这种系统直接使用api层即可无需rpc服务
- 各个功能的使用场景以及使用案例
- 限流
- 熔断
- 降载
- 超时
- 可观测性
- go-zero的实际体验
- 服务很稳
- 前后端接口一致性一个api文件即可生成前后端代码
- 规范、代码量少意味着bug少
- 免除api文档极大降低沟通成本
- 代码结构完全一致,便于维护和接手
- 微服务的项目结构, monorepo的 CICD 处理
```
bookstore
├── api
│   ├── etc
│   └── internal
│   ├── config
│   ├── handler
│   ├── logic
│   ├── svc
│   └── types
└── rpc
├── add
│   ├── adder
│   ├── etc
│   ├── internal
│   │   ├── config
│   │   ├── logic
│   │   ├── server
│   │   └── svc
│   └── pb
├── check
│   ├── checker
│   ├── etc
│   ├── internal
│   │   ├── config
│   │   ├── logic
│   │   ├── server
│   │   └── svc
│   └── pb
└── model
```
mono repo的CI我们是通过gitlab做的CD使用jenkins
CI尽可能更严格的模式比如-race使用sonar等工具
CD有开发、测试、预发、灰度和正式集群
晚6点上灰度、无故障的话第二天10点自动同步到正式集群
正式集群分为多个k8s集群有效的防止单集群故障直接摘除即可集群升级更有好
- 如何部署,如何监控?
- 全量K8S通过jenkins自动打包成docker镜像按照时间打包tag这样可以一眼看出哪一天的镜像
- 上面已经讲了,预发->灰度->正式
- Prometheus+自建dashboard服务
- 基于日志检测服务和请求异常
- 如果打算换go-zero框架重构业务如何做好线上业务稳定安全用户无感切换另外咨询下如何进行服务划分
- 逐步替换从外到内加个proxy来校对校对一周后可以切换
- 如有数据库重构,则需要做好新老同步
- 服务划分按照业务来遵循从粗到细的原则避免一个api一个微服务
- 数据拆分对于微服务来讲尤为重要,上层好拆,数据难拆,尽可能保证按照业务拆分数据
- 服务发现
- 服务发现 etcd 的 key 的设计
- 服务key+时间戳,服务进程数存在时间戳冲突的概率极低,忽略
- etcd服务发现与治理 异常捕获与处理异常
- 为啥k8s还使用etcd做服务发现因为dns的刷新有延迟导致滚动更新会有大量失败而etcd可以做到完全无损更新
- etcd集群直接部署在k8s集群内因为多个正式集群集群单点和注册避免混乱
- 针对etcd异常或者leader切换自动侦测并刷新当etcd有异常不能恢复时不会刷新服务列表保障服务依然可用
- 缓存的设计与使用案例
- 分布式多redis集群线上最大几十个集群为同一个服务提供缓存服务
- 无缝扩缩容
- 不存在没有过期时间的缓存,避免大量不常使用的数据占用资源,默认一周
- 缓存穿透,没有的数据会短暂缓存一分钟,避免刷接口或大量不存在的数据请求带垮系统
- 缓存击穿,一个进程只会刷新一次同一个数据,避免热点数据被大量同时加载
- 缓存雪崩对缓存过期时间自动做了jitter5%的标准变差使得一周的过期时间分布在16小时内有效防止了雪崩
- 我们线上数据库都有缓存,否则无法支撑海量并发
- 自动缓存管理已经内置于go-zero并可以通过goctl自动生成代码
- 能否讲解下, 中间件,拦截器的设计思想
- 洋葱模型
- 本中间件处理比如限流熔断等然后决定是否调用next
- next调用
- 对next调用返回结果做处理
- 微服务的事务处理怎么实现好gozero分布式事务设计和实现有什么好中间件推荐
- 2PC两阶段提交
- TCCTry-Confirm-Cancel
- 消息队列,最大尝试
- 人工补偿
- 多级 goroutine 的异常捕获 ,怎么设计比较好
- 微服务系统请求异常应该隔离,不能让单个异常请求带崩整个进程
- go-zero自带了RunSafe/GoSafe用来防止单个异常请求导致进程崩溃
- 监控需要跟上,防止异常过量而不自知
- fail fast和故障隔离的矛盾点
- k8s配置的生成与使用(gateway, service, slb)
- 内部自动生成k8s的yaml文件过于依赖配置而未开源
- 打算在bookstore的示例里加上k8s配置样板
- slb->nginx->nodeport->api gateway->rpc service
- gateway限流、熔断和降载
- 限流分为两种:并发控制和分布式限流
- 并发控制用来防止瞬间过量请求,保护系统不被打垮
- 分布式限流用来给不同服务配置不同的quota
- 熔断是为了对依赖的服务进行保护当一个服务出现大量异常的时候调用者应该给予保护使其有机会恢复正常同时也达到fail fast的效果
- 降载是为了保护当前进程资源耗尽而陷入彻底不可用,确保尽可能服务好能承载的最大请求量
- 降载配合k8s可以有效保护k8s扩容k8s扩容分钟级go-zero降载秒级
- 介绍core中好用的组件如timingwheel等讲讲设计思路
- 布隆过滤器
- 进程内cache
- RollingWindow
- TimingWheel
- 各种executors
- fx包map/reduce/filter/sort/group/distinct/head/tail...
- 一致性hash实现
- 分布式限流实现
- mapreduce带cancel能力
- syncx包里有大量的并发工具
- 如何快速增加一种rpc协议支持將跨机发现改为调本机节点并关闭复杂filter和负载均衡功能
- go-zero跟grpc关系还是比较紧密的设计之初没有考虑支持grpc以外的协议
- 如果要增加的话那就只能fork出来魔改了
- 调本机直接用direct的scheme即可
- 为啥要去掉filter和负载均衡如果要去的话fork了改但没必要
- 日志和监控和链路追踪的设计和实现思路,最好有大概图解
- 日志和监控我们使用prometheus, 自定义dashboard服务捆绑提交数据每分钟
- 链路追踪可以看出调用关系自动记录trace日志
![](https://lh5.googleusercontent.com/PBRdYmRs22xEH1gjNkQnoHuB5WFBva10oKCm61A6G23xvi28u95Bwq-qTc_WVV-PihzAHyLpAKkBtbtzK8v9Kjtrp3YBZqGiTSXhHJHwf7CAv5K9AqBSc1CZuV0u3URCDVP8r1RD0PY#align=left&display=inline&height=658&margin=%5Bobject%20Object%5D&originHeight=658&originWidth=1294&status=done&style=none&width=1294)
- go-zero框架有用到什么池化技术吗如果有在哪些core代码里面可以参考
- 一般不需要提前优化,过度优化是大忌
- core/syncx/pool.go里面定义了带过期时间的通用池化技术
- go-zero用到了那些性能测试方法框架有代码参考吗可以说说思路和经验
- go benchmark
- 压测可以通过现有业务日志样本,来按照预估等比放大
- 压测一定要压到系统扛不住,看第一个瓶颈在哪里,改完再压,循环
- 说一下代码的抽象经验和心得
- Dont repeat yourself
- 你未必需要它,之前经常有业务开发人员问我可不可以增加这个功能或那个功能,我一般都会仔细询问深层次目的,很多时候会发现其实这个功能是多余的,不需要才是最佳实践
- Martin Fowler提出出现三次再抽象的原则有时有些同事会找我往框架里增加一个功能我思考后经常会回答这个你先在业务层写其它地方也有需要了你再告诉我三次出现我会考虑集成到框架里
- 一个文件应该尽量只做一件事每个文件尽可能控制在200行以内一个函数尽可能控制在50行以内这样不需要滚动就可以看到整个函数
- 需要抽象和提炼的能力,多去思考,经常回头思考之前的架构或实现
- 你会就go-zero 框架从设计到实践出书吗?框架以后的发展规划是什么?
- 暂无出书计划,做好框架是最重要的
- 继续着眼于工程效率
- 提升服务治理能力
- 帮助业务开发尽可能快速落地

View File

@ -0,0 +1,127 @@
# periodlimit
不管是在单体服务中还是在微服务中开发者为前端提供的API接口都是有访问上限的当访问频率或者并发量超过其承受范围时候我们就必须考虑限流来保证接口的可用性或者降级可用性。即接口也需要安装上保险丝以防止非预期的请求对系统压力过大而引起的系统瘫痪。
本文就来介绍一下 `periodlimit` 。
## 使用
```go
const (
seconds = 1
total = 100
quota = 5
)
// New limiter
l := NewPeriodLimit(seconds, quota, redis.NewRedis(s.Addr(), redis.NodeType), "periodlimit")
// take source
code, err := l.Take("first")
if err != nil {
logx.Error(err)
return true
}
// switch val => process request
switch code {
case limit.OverQuota:
logx.Errorf("OverQuota key: %v", key)
return false
case limit.Allowed:
logx.Infof("AllowedQuota key: %v", key)
return true
case limit.HitQuota:
logx.Errorf("HitQuota key: %v", key)
// todo: maybe we need to let users know they hit the quota
return false
default:
logx.Errorf("DefaultQuota key: %v", key)
// unknown response, we just let the sms go
return true
}
```
## periodlimit
`go-zero` 采取 **滑动窗口** 计数的方式,计算一段时间内对同一个资源的访问次数,如果超过指定的 `limit` ,则拒绝访问。当然如果你是在一段时间内访问不同的资源,每一个资源访问量都不超过 `limit` ,此种情况是允许大量请求进来的。
而在一个分布式系统中,存在多个微服务提供服务。所以当瞬间的流量同时访问同一个资源,如何让计数器在分布式系统中正常计数? 同时在计算资源访问时,可能会涉及多个计算,如何保证计算的原子性?
- `go-zero` 借助 `redis``incrby` 做资源访问计数
- 采用 `lua script` 做整个窗口计算,保证计算的原子性
下面来看看 `lua script` 控制的几个关键属性:
| **argument** | **mean** |
| --- | --- |
| key[1] | 访问资源的标示 |
| ARGV[1] | limit => 请求总数,超过则限速。可设置为 QPS |
| ARGV[2] | window大小 => 滑动窗口,用 ttl 模拟出滑动的效果 |
```lua
-- to be compatible with aliyun redis,
-- we cannot use `local key = KEYS[1]` to reuse thekey
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
-- incrbt key 1 => key visis++
local current = redis.call("INCRBY", KEYS[1], 1)
-- 如果是第一次访问,设置过期时间 => TTL = window size
-- 因为是只限制一段时间的访问次数
if current == 1 then
redis.call("expire", KEYS[1], window)
return 1
elseif current < limit then
return 1
elseif current == limit then
return 2
else
return 0
end
```
至于上述的 `return code` ,返回给调用方。由调用方来决定请求后续的操作:
| **return code** | **tag** | call code | **mean** |
| --- | --- | --- | --- |
| 0 | OverQuota | 3 | **over limit** |
| 1 | Allowed | 1 | **in limit** |
| 2 | HitQuota | 2 | **hit limit** |
下面这张图描述了请求进入的过程,以及请求触发 `limit` 时后续发生的情况:
![image.png](https://cdn.nlark.com/yuque/0/2020/png/261626/1605430483430-92415ed3-e88f-487d-8fd6-8c58a9abe334.png#align=left&display=inline&height=524&margin=%5Bobject%20Object%5D&name=image.png&originHeight=524&originWidth=1051&size=90836&status=done&style=none&width=1051)
![image.png](https://cdn.nlark.com/yuque/0/2020/png/261626/1605495120249-f6b05ac2-7090-47b0-a3c0-da50df6206dd.png#align=left&display=inline&height=557&margin=%5Bobject%20Object%5D&name=image.png&originHeight=557&originWidth=456&size=53785&status=done&style=none&width=456)
## 后续处理
如果在服务某个时间点,请求大批量打进来,`periodlimit` 短期时间内达到 `limit` 阈值,而且设置的时间范围还远远没有到达。后续请求的处理就成为问题。
`periodlimit` 中并没有处理,而是返回 `code` 。把后续请求的处理交给了开发者自己处理。
1. 如果不做处理,那就是简单的将请求拒绝
1. 如果需要处理这些请求,开发者可以借助 `mq` 将请求缓冲,减缓请求的压力
1. 采用 `tokenlimit`,允许暂时的流量冲击
所以下一篇我们就来聊聊 `tokenlimit`
## 总结
`go-zero` 中的 `periodlimit` 限流方案是基于 `redis` 计数器,通过调用 `redis lua script` ,保证计数过程的原子性,同时保证在分布式的情况下计数是正常的。但是这种方案存在缺点,因为它要记录时间窗口内的所有行为记录,如果这个量特别大的时候,内存消耗会变得非常严重。
## 参考
- [go-zero periodlimit](https://github.com/tal-tech/go-zero/blob/master/core/limit/periodlimit.go)
- [分布式服务限流实战,已经为你排好坑了](https://www.infoq.cn/article/Qg2tX8fyw5Vt-f3HH673)
- [tokenlimit](tokenlimit.md)

View File

@ -0,0 +1,16 @@
# 插件中心
goctl api提供了对plugin命令来支持对api进行功能扩展当goctl api中的功能不满足你的使用
或者需要对goctl api进行功能自定义的扩展那么插件功能将非常适合开发人员进行自给自足详情见
[goctl plugin](goctl-plugin.md)
## 插件资源
* [goctl-go-compact](https://github.com/zeromicro/goctl-go-compact)
goctl默认的一个路由一个文件合并成一个文件
* [goctl-swagger](https://github.com/zeromicro/goctl-swagger)
通过api文件生成swagger文档
* [goctl-php](https://github.com/zeromicro/goctl-php)
goctl-php是一款基于goctl的插件用于生成 php 调用端(服务端) http server请求代码
# 猜你想看
* [goctl插件](goctl-plugin.md)
* [api语法介绍](api-grammar.md)

View File

@ -0,0 +1,9 @@
# 其他
在之前我们已经对Go环境、Go Module配置、Goctl、protoc&protoc-gen-go安装准备就绪这些是开发人员在开发阶段必须要准备的环境而接下来的环境你可以选择性的安装
因为这些环境一般存在于服务器(安装工作运维会替你完成),但是为了后续**演示**流程能够完整走下去,我建议大家在本地也安装一下,因为我们的演示环境大部分会以本地为主。
以下仅给出了需要的准备工作,不以文档篇幅作详细介绍了。
## 其他环境
* [etcd](https://etcd.io/docs/current/rfc/v3api/)
* [redis](https://redis.io/)
* [mysql](https://www.mysql.com/)

View File

@ -0,0 +1,8 @@
# 准备工作
在正式进入实际开发之前我们需要做一些准备工作比如Go环境的安装grpc代码生成使用的工具安装
必备工具Goctl的安装Golang环境配置等本节将包含以下小节
* [golang安装](golang-install.md)
* [go modudle配置](gomod-config.md)
* [goctl安装](goctl-install.md)
* [protoc&protoc-gen-go安装](protoc-install.md)
* [其他](prepare-other.md)

View File

@ -0,0 +1,32 @@
# 项目开发
在前面的章节我们已经从一些概念、背景、快速入门等维度介绍了一下go-zero看到这里相信你对go-zero已经有了一些了解
从这里开始我们将会从环境准备到服务部署整个流程开始进行讲解为了保证大家能够彻底弄懂go-zero的开发流程那就准备你的耐心来接着往下走吧。
在章节中,将包含以下小节:
* [准备工作](prepare.md)
* [golang安装](golang-install.md)
* [go modudle配置](gomod-config.md)
* [goctl安装](goctl-install.md)
* [protoc&protoc-gen-go安装](protoc-install.md)
* [其他](prepare-other.md)
* [开发规范](dev-specification.md)
* [命名规范](naming-spec.md)
* [路由规范](route-naming-spec.md)
* [编码规范](coding-spec.md)
* [开发流程](dev-flow.md)
* [配置介绍](config-introduction.md)
* [api配置](api-config.md)
* [rpc配置](rpc-config.md)
* [业务开发](business-dev.md)
* [目录拆分](service-design.md)
* [model生成](model-gen.md)
* [api文件编写](api-coding.md)
* [业务编码](business-coding.md)
* [jwt鉴权](jwt.md)
* [中间件使用](middleware.md)
* [rpc服务编写与调用](rpc-call.md)
* [错误处理](error-handle.md)
* [CI/CD](ci-cd.md)
* [服务部署](service-deployment.md)
* [日志收集](log-collection.md)
* [链路追踪](trace.md)
* [服务监控](service-monitor.md)

View File

@ -0,0 +1,56 @@
# protoc & protoc-gen-go安装
## 前言
protoc是一款用C++编写的工具其可以将proto文件翻译为指定语言的代码。在go-zero的微服务中我们采用grpc进行服务间的通信而grpc的编写就需要用到protoc和翻译成go语言rpc stub代码的插件protoc-gen-go。
本文演示环境
* mac OS
* protoc 3.14.0
## protoc安装
* 进入[protobuf release](https://github.com/protocolbuffers/protobuf/releases) 页面,选择适合自己操作系统的压缩包文件
* 解压`protoc-3.14.0-osx-x86_64.zip`并进入`protoc-3.14.0-osx-x86_64`
```shell
$ cd protoc-3.14.0-osx-x86_64/bin
```
* 将启动的`protoc`二进制文件移动到被添加到环境变量的任意path下如`$GOPATH/bin`这里不建议直接将其和系统的以下path放在一起。
```shell
$ mv protoc $GOPATH/bin
```
> [!TIP]
> $GOPATH为你本机的实际文件夹地址
* 验证安装结果
```shell
$ protoc --version
```
```shell
libprotoc 3.14.0
```
## protoc-gen-*安装
在goctl版本大于1.2.1时,则不需要安装 `protoc-gen-go` 插件了因为在该版本以后goctl已经实现了作为 `protoc` 的插件了goctl在指定 `goctl xxx` 命令时会自动
`goctl` 创建一个符号链接 `protoc-gen-goctl` 在生成pb.go时会按照如下逻辑生成
1. 检测环境变量中是否存在 `protoc-gen-goctl` 插件如果是则跳转到第3步
2. 检测环境变量中是否存在 `protoc-gen-go` 插件,如果不存在,则生成流程结束
3. 根据检测到的插件生成pb.go
> [!TIPS]
>
> Windows 在创建符号链接可能会报错, `A required privilege is not held by the client.`, 原因是在Windows下执行goctl需要"以管理员身份"运行。
>
* 下载安装`protoc-gen-go`
如果goctl 版本已经是1.2.1以后了,可以忽略此步骤。
```shell
$ go get -u github.com/golang/protobuf/protoc-gen-go@v1.3.2
```
```text
go: found github.com/golang/protobuf/protoc-gen-go in github.com/golang/protobuf v1.4.3
go: google.golang.org/protobuf upgrade => v1.25.0
```
* 将protoc-gen-go移动到被添加环境变量的任意path下如`$GOPATH/bin`,由于`go get`后的二进制本身就在`$GOPATH/bin`目录中,因此只要确保你的`$GOPATH/bin`在环境变量即可。
> **[!WARNING]
> protoc-gen-go安装失败请阅读[常见错误处理](error.md)

View File

@ -0,0 +1,6 @@
# 快速开发
本节主要通过对 api/rpc 等服务快速开始来让大家对使用 go-zero 开发的工程有一个宏观概念,更加详细的介绍我们将在后续一一展开。如果您已经参考 [准备工作](prepare.md) 做好环境及工具的准备,请跟随以下小节开始体验:
* [单体服务](monolithic-service.md)
* [微服务](micro-service.md)

View File

@ -0,0 +1,269 @@
# go-zero缓存设计之持久层缓存
## 缓存设计原理
我们对缓存是只删除不做更新一旦DB里数据出现修改我们就会直接删除对应的缓存而不是去更新。
我们看看删除缓存的顺序怎样才是正确的。
* 先删除缓存再更新DB
![redis-cache-01](./resource/redis-cache-01.png)
我们看两个并发请求的情况A请求需要更新数据先删除了缓存然后B请求来读取数据此时缓存没有数据就会从DB加载数据并写回缓存然后A更新了DB那么此时缓存内的数据就会一直是脏数据直到缓存过期或者有新的更新数据的请求。如图
![redis-cache-02](./resource/redis-cache-02.png)
* 先更新DB再删除缓存
![redis-cache-03](./resource/redis-cache-03.png)
A请求先更新DB然后B请求来读取数据此时返回的是老数据此时可以认为是A请求还没更新完最终一致性可以接受然后A删除了缓存后续请求都会拿到最新数据如图
![redis-cache-04](./resource/redis-cache-04.png)
让我们再来看一下正常的请求流程:
* 第一个请求更新DB并删除了缓存
* 第二个请求读取缓存没有数据就从DB读取数据并回写到缓存里
* 后续读请求都可以直接从缓存读取
![redis-cache-05](./resource/redis-cache-05.png)
我们再看一下DB查询有哪些情况假设行记录里有ABCDEFG七列数据
* 只查询部分列数据的请求比如请求其中的ABCCDE或者EFG等如图
![redis-cache-06](./resource/redis-cache-06.png)
* 查询单条完整行记录,如图
![redis-cache-07](./resource/redis-cache-07.png)
* 查询多条行记录的部分或全部列,如图
![redis-cache-08](./resource/redis-cache-08.png)
对于上面三种情况首先我们不用部分查询因为部分查询没法缓存一旦缓存了数据有更新没法定位到有哪些数据需要删除其次对于多行的查询根据实际场景和需要我们会在业务层建立对应的从查询条件到主键的映射而对于单行完整记录的查询go-zero 内置了完整的缓存管理方式。所以核心原则是:**go-zero 缓存的一定是完整的行记录**。
下面我们来详细介绍 go-zero 内置的三种场景的缓存处理方式:
* 基于主键的缓存
```text
PRIMARY KEY (`id`)
```
这种相对来讲是最容易处理的缓存,只需要在 redis 里用 primary key 作为 key 来缓存行记录即可。
* 基于唯一索引的缓存
![redis-cache-09](./resource/redis-cache-09.webp)
在做基于索引的缓存设计的时候我借鉴了 database 索引的设计方法,在 database 设计里,如果通过索引去查数据时,引擎会先在 索引->主键 的 tree 里面查找到主键,然后再通过主键去查询行记录,就是引入了一个间接层去解决索引到行记录的对应问题。在 go-zero 的缓存设计里也是同样的原理。
基于索引的缓存又分为单列唯一索引和多列唯一索引:
但是对于 go-zero 来说,单列和多列只是生成缓存 key 的方式不同而已,背后的控制逻辑是一样的。然后 go-zero 内置的缓存管理就比较好的控制了数据一致性问题,同时也内置防止了缓存的击穿、穿透、雪崩问题(这些在 gopherchina 大会上分享的时候仔细讲过,见后续 gopherchina 分享视频)。
另外go-zero 内置了缓存访问量、访问命中率统计,如下所示:
```text
dbcache(sqlc) - qpm: 5057, hit_ratio: 99.7%, hit: 5044, miss: 13, db_fails: 0
```
可以看到比较详细的统计信息,便于我们来分析缓存的使用情况,对于缓存命中率极低或者请求量极小的情况,我们就可以去掉缓存了,这样也可以降低成本。
* 单列唯一索引如下:
```text
UNIQUE KEY `product_idx` (`product`)
```
* 多列唯一索引如下:
```text
UNIQUE KEY `vendor_product_idx` (`vendor`, `product`)
```
## 缓存代码解读
### 1.基于主键的缓存逻辑
![redis-cache-10](./resource/redis-cache-10.png)
具体实现代码如下:
```go
func (cc CachedConn) QueryRow(v interface{}, key string, query QueryFn) error {
return cc.cache.Take(v, key, func(v interface{}) error {
return query(cc.db, v)
})
}
```
这里的 `Take` 方法是先从缓存里去通过 `key` 拿数据,如果拿到就直接返回,如果拿不到,那么就通过 `query` 方法去 `DB` 读取完整行记录并写回缓存,然后再返回数据。整个逻辑还是比较简单易懂的。
我们详细看看 `Take` 的实现:
```go
func (c cacheNode) Take(v interface{}, key string, query func(v interface{}) error) error {
return c.doTake(v, key, query, func(v interface{}) error {
return c.SetCache(key, v)
})
}
```
`Take` 的逻辑如下:
* 用 key 从缓存里查找数据
* 如果找到,则返回数据
* 如果找不到,用 query 方法去读取数据
* 读到后调用 c.SetCache(key, v) 设置缓存
其中的 `doTake` 代码和解释如下:
```go
// v - 需要读取的数据对象
// key - 缓存key
// query - 用来从DB读取完整数据的方法
// cacheVal - 用来写缓存的方法
func (c cacheNode) doTake(v interface{}, key string, query func(v interface{}) error,
cacheVal func(v interface{}) error) error {
// 用barrier来防止缓存击穿确保一个进程内只有一个请求去加载key对应的数据
val, fresh, err := c.barrier.DoEx(key, func() (interface{}, error) {
// 从cache里读取数据
if err := c.doGetCache(key, v); err != nil {
// 如果是预先放进来的placeholder用来防止缓存穿透那么就返回预设的errNotFound
// 如果是未知错误那么就直接返回因为我们不能放弃缓存出错而直接把所有请求去请求DB
// 这样在高并发的场景下会把DB打挂掉的
if err == errPlaceholder {
return nil, c.errNotFound
} else if err != c.errNotFound {
// why we just return the error instead of query from db,
// because we don't allow the disaster pass to the DBs.
// fail fast, in case we bring down the dbs.
return nil, err
}
// 请求DB
// 如果返回的error是errNotFound那么我们就需要在缓存里设置placeholder防止缓存穿透
if err = query(v); err == c.errNotFound {
if err = c.setCacheWithNotFound(key); err != nil {
logx.Error(err)
}
return nil, c.errNotFound
} else if err != nil {
// 统计DB失败
c.stat.IncrementDbFails()
return nil, err
}
// 把数据写入缓存
if err = cacheVal(v); err != nil {
logx.Error(err)
}
}
// 返回json序列化的数据
return jsonx.Marshal(v)
})
if err != nil {
return err
}
if fresh {
return nil
}
// got the result from previous ongoing query
c.stat.IncrementTotal()
c.stat.IncrementHit()
// 把数据写入到传入的v对象里
return jsonx.Unmarshal(val.([]byte), v)
}
```
### 2. 基于唯一索引的缓存逻辑
因为这块比较复杂,所以我用不同颜色标识出来了响应的代码块和逻辑,`block 2` 其实跟基于主键的缓存是一样的,这里主要讲 `block 1` 的逻辑。
![redis-cache-11](./resource/redis-cache-11.webp)
代码块的 block 1 部分分为两种情况:
* 通过索引能够从缓存里找到主键,此时就直接用主键走 `block 2` 的逻辑了,后续同上面基于主键的缓存逻辑
* 通过索引无法从缓存里找到主键
* 通过索引从DB里查询完整行记录如有 `error`,返回
* 查到完整行记录后,会把主键到完整行记录的缓存和索引到主键的缓存同时写到 `redis`
* 返回所需的行记录数据
```go
// v - 需要读取的数据对象
// key - 通过索引生成的缓存key
// keyer - 用主键生成基于主键缓存的key的方法
// indexQuery - 用索引从DB读取完整数据的方法需要返回主键
// primaryQuery - 用主键从DB获取完整数据的方法
func (cc CachedConn) QueryRowIndex(v interface{}, key string, keyer func(primary interface{}) string,
indexQuery IndexQueryFn, primaryQuery PrimaryQueryFn) error {
var primaryKey interface{}
var found bool
// 先通过索引查询缓存,看是否有索引到主键的缓存
if err := cc.cache.TakeWithExpire(&primaryKey, key, func(val interface{}, expire time.Duration) (err error) {
// 如果没有索引到主键的缓存,那么就通过索引查询完整数据
primaryKey, err = indexQuery(cc.db, v)
if err != nil {
return
}
// 通过索引查询到了完整数据设置found后面直接使用不需要再从缓存读取数据了
found = true
// 将主键到完整数据的映射保存到缓存里TakeWithExpire方法已经将索引到主键的映射保存到缓存了
return cc.cache.SetCacheWithExpire(keyer(primaryKey), v, expire+cacheSafeGapBetweenIndexAndPrimary)
}); err != nil {
return err
}
// 已经通过索引找到了数据,直接返回即可
if found {
return nil
}
// 通过主键从缓存读取数据如果缓存没有通过primaryQuery方法从DB读取并回写缓存再返回数据
return cc.cache.Take(v, keyer(primaryKey), func(v interface{}) error {
return primaryQuery(cc.db, v, primaryKey)
})
}
```
我们来看一个实际的例子
```go
func (m *defaultUserModel) FindOneByUser(user string) (*User, error) {
var resp User
// 生成基于索引的key
indexKey := fmt.Sprintf("%s%v", cacheUserPrefix, user)
err := m.QueryRowIndex(&resp, indexKey,
// 基于主键生成完整数据缓存的key
func(primary interface{}) string {
return fmt.Sprintf("user#%v", primary)
},
// 基于索引的DB查询方法
func(conn sqlx.SqlConn, v interface{}) (i interface{}, e error) {
query := fmt.Sprintf("select %s from %s where user = ? limit 1", userRows, m.table)
if err := conn.QueryRow(&resp, query, user); err != nil {
return nil, err
}
return resp.Id, nil
},
// 基于主键的DB查询方法
func(conn sqlx.SqlConn, v, primary interface{}) error {
query := fmt.Sprintf("select %s from %s where id = ?", userRows, m.table)
return conn.QueryRow(&resp, query, primary)
})
// 错误处理需要判断是否返回的是sqlc.ErrNotFound如果是我们用本package定义的ErrNotFound返回
// 避免使用者感知到有没有使用缓存,同时也是对底层依赖的隔离
switch err {
case nil:
return &resp, nil
case sqlc.ErrNotFound:
return nil, ErrNotFound
default:
return nil, err
}
}
```
所有上面这些缓存的自动管理代码都是可以通过 [goctl](goctl.md) 自动生成的,我们团队内部 `CRUD` 和缓存基本都是通过 [goctl](goctl.md) 自动生成的,可以节省大量开发时间,并且缓存代码本身也是非常容易出错的,即使有很好的代码经验,也很难每次完全写对,所以我们推荐尽可能使用自动的缓存代码生成工具去避免错误。
# 猜你想看
* [Go开源说第四期-go-zero缓存如何设计](https://www.bilibili.com/video/BV1Jy4y127Xu)
* [Goctl](goctl.md)

View File

@ -0,0 +1,142 @@
# redis-lock
# redis lock
既然是锁,首先想到的一个作用就是:**防重复点击,在一个时间点只有一个请求产生效果**。
而既然是 `redis`,就得具有排他性,同时也具有锁的一些共性:
- 高性能
- 不能出现死锁
- 不能出现节点down掉后加锁失败
`go-zero` 中利用 redis `set key nx` 可以保证key不存在时写入成功`px` 可以让key超时后自动删除「最坏情况也就是超时自动删除key从而也不会出现死锁」
## example
```go
redisLockKey := fmt.Sprintf("%v%v", redisTpl, headId)
// 1. New redislock
redisLock := redis.NewRedisLock(redisConn, redisLockKey)
// 2. 可选操作,设置 redislock 过期时间
redisLock.SetExpire(redisLockExpireSeconds)
if ok, err := redisLock.Acquire(); !ok || err != nil {
return nil, errors.New("当前有其他用户正在进行操作,请稍后重试")
}
defer func() {
recover()
// 3. 释放锁
redisLock.Release()
}()
```
和你在使用 `sync.Mutex` 的方式时一致的。加锁解锁,执行你的业务操作。
## 获取锁
```go
lockCommand = `if redis.call("GET", KEYS[1]) == ARGV[1] then
redis.call("SET", KEYS[1], ARGV[1], "PX", ARGV[2])
return "OK"
else
return redis.call("SET", KEYS[1], ARGV[1], "NX", "PX", ARGV[2])
end`
func (rl *RedisLock) Acquire() (bool, error) {
seconds := atomic.LoadUint32(&rl.seconds)
// execute luascript
resp, err := rl.store.Eval(lockCommand, []string{rl.key}, []string{
rl.id, strconv.Itoa(int(seconds)*millisPerSecond + tolerance)})
if err == red.Nil {
return false, nil
} else if err != nil {
logx.Errorf("Error on acquiring lock for %s, %s", rl.key, err.Error())
return false, err
} else if resp == nil {
return false, nil
}
reply, ok := resp.(string)
if ok && reply == "OK" {
return true, nil
} else {
logx.Errorf("Unknown reply when acquiring lock for %s: %v", rl.key, resp)
return false, nil
}
}
```
先介绍几个 `redis` 的命令选项,以下是为 `set` 命令增加的选项:
- `ex seconds` 设置key过期时间单位s
- `px milliseconds` 设置key过期时间单位毫秒
- `nx`key不存在时设置key的值
- `xx`key存在时才会去设置key的值
其中 `lua script` 涉及的入参:
| args | 示例 | 含义 |
| --- | --- | --- |
| KEYS[1] | key$20201026 | redis key |
| ARGV[1] | lmnopqrstuvwxyzABCD | 唯一标识:随机字符串 |
| ARGV[2] | 30000 | 设置锁的过期时间 |
然后来说说代码特性:
1. `Lua` 脚本保证原子性「当然,把多个操作在 Redis 中实现成一个操作,也就是单命令操作」
1. 使用了 `set key value px milliseconds nx`
1. `value` 具有唯一性
1. 加锁时首先判断 `key``value` 是否和之前设置的一致,一致则修改过期时间
## 释放锁
```go
delCommand = `if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end`
func (rl *RedisLock) Release() (bool, error) {
resp, err := rl.store.Eval(delCommand, []string{rl.key}, []string{rl.id})
if err != nil {
return false, err
}
if reply, ok := resp.(int64); !ok {
return false, nil
} else {
return reply == 1, nil
}
}
```
释放锁的时候只需要关注一点:
**不能释放别人的锁,不能释放别人的锁,不能释放别人的锁**
所以需要先 `get(key) == value「key」`,为 true 才会去 `delete`

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

Some files were not shown because too many files have changed in this diff Show More