diff --git a/.github/workflows/build.sh b/.github/workflows/build.sh new file mode 100644 index 00000000..4129a574 --- /dev/null +++ b/.github/workflows/build.sh @@ -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 \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..a7230d4c --- /dev/null +++ b/.github/workflows/build.yml @@ -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 diff --git a/.github/workflows/contributor-tool b/.github/workflows/contributor-tool new file mode 100755 index 00000000..7c87f444 Binary files /dev/null and b/.github/workflows/contributor-tool differ diff --git a/LICENSE b/LICENSE index aba75d30..cab46154 100644 --- a/LICENSE +++ b/LICENSE @@ -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 diff --git a/doc/breaker.md b/doc/breaker.md deleted file mode 100644 index 2d49ef50..00000000 --- a/doc/breaker.md +++ /dev/null @@ -1,5 +0,0 @@ -# 熔断机制设计 - -## 设计目的 - -* 依赖的服务出现大规模故障时,调用方应该尽可能少调用,降低故障服务的压力,使之尽快恢复服务 \ No newline at end of file diff --git a/doc/goctl-model-sql.md b/doc/goctl-model-sql.md deleted file mode 100644 index 1a9925b7..00000000 --- a/doc/goctl-model-sql.md +++ /dev/null @@ -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外,其他的代码均属于业务型代码,这个我觉得开发人员根据业务需要进行编写更好。 - diff --git a/doc/goctl.md b/doc/goctl.md deleted file mode 100644 index f954ef23..00000000 --- a/doc/goctl.md +++ /dev/null @@ -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基本信息,比如Auth,api是哪个用途。 - -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, redis,rpc等 -* 在定义的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) diff --git a/doc/jwt.md b/doc/jwt.md deleted file mode 100644 index 6eaadfdf..00000000 --- a/doc/jwt.md +++ /dev/null @@ -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了,一般都需要设置过期时间前几天。 - diff --git a/doc/periodicalexecutor.md b/doc/periodicalexecutor.md deleted file mode 100644 index 37968869..00000000 --- a/doc/periodicalexecutor.md +++ /dev/null @@ -1,15 +0,0 @@ -# PeriodicalExecutor设计 - -## 添加任务 - -* 当前没有未执行的任务 - * 添加并启动定时器 -* 已有未执行的任务 - * 添加并检查是否到达最大缓存数 - * 如到,执行所有缓存任务 - * 未到,只添加 - -## 定时器到期 - -* 清除并执行所有缓存任务 -* 再等待N个定时周期,如果等待过程中一直没有新任务,则退出 diff --git a/doc/shorturl-en.md b/doc/shorturl-en.md index ee365c3f..f5434566 100644 --- a/doc/shorturl-en.md +++ b/doc/shorturl-en.md @@ -1,3 +1,5 @@ +# DEPRECATED: PLEASE MOVE TO https://go-zero.dev/cn/extended-reading.html + # Rapid development of microservices English | [简体中文](shorturl.md) @@ -19,11 +21,11 @@ To build a well working microservice, we need lots of knowledges from different 3. logging, collects data and helps to backtrace problems 4. observability, no metrics, no optimization -For any point listed above, we need a long article to describe the theory and the implementation. But for us, the developers, it’s very difficult to understand all the concepts and make it happen in our systems. Although, we can use the frameworks that have been well served busy sites. [go-zero](https://github.com/tal-tech/go-zero) is born for this purpose, especially for cloud-native microservice systems. +For any point listed above, we need a long article to describe the theory and the implementation. But for us, the developers, it’s very difficult to understand all the concepts and make it happen in our systems. Although, we can use the frameworks that have been well served busy sites. [go-zero](https://github.com/zeromicro/go-zero) is born for this purpose, especially for cloud-native microservice systems. 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`. -Let’s 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, you’ll find that it’s so easy to write microservices! +Let’s 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, you’ll find that it’s so easy to write microservices! ## 1. What is a shorturl service diff --git a/doc/shorturl.md b/doc/shorturl.md index 8277d962..38b8c1fc 100644 --- a/doc/shorturl.md +++ b/doc/shorturl.md @@ -1,3 +1,5 @@ +# DEPRECATED: PLEASE MOVE TO https://go-zero.dev/cn/extended-reading.html + # 快速构建高并发微服务 [English](shorturl-en.md) | 简体中文 @@ -19,11 +21,11 @@ 3. 日志,用于数据收集和问题定位 4. 可观测性,没有度量就没有优化 -对于其中每一点,我们都需要用很长的篇幅来讲述其原理和实现,那么对我们后端开发者来说,要想把这些知识点都掌握并落实到业务系统里,难度是非常大的,不过我们可以依赖已经被大流量验证过的框架体系。[go-zero 微服务框架](https://github.com/tal-tech/go-zero)就是为此而生。 +对于其中每一点,我们都需要用很长的篇幅来讲述其原理和实现,那么对我们后端开发者来说,要想把这些知识点都掌握并落实到业务系统里,难度是非常大的,不过我们可以依赖已经被大流量验证过的框架体系。[go-zero 微服务框架](https://github.com/zeromicro/go-zero)就是为此而生。 另外,我们始终秉承 **工具大于约定和文档** 的理念。我们希望尽可能减少开发人员的心智负担,把精力都投入到产生业务价值的代码上,减少重复代码的编写,所以我们开发了 `goctl` 工具。 -下面我通过短链微服务来演示通过 [go-zero](https://github.com/tal-tech/go-zero) 快速的创建微服务的流程,走完一遍,你就会发现:原来编写微服务如此简单! +下面我通过短链微服务来演示通过 [go-zero](https://github.com/zeromicro/go-zero) 快速的创建微服务的流程,走完一遍,你就会发现:原来编写微服务如此简单! ## 1. 什么是短链服务 diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 89963c75..8aa58ede 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -27,7 +27,7 @@ module.exports = { link: "/zero/", }, { - text: "go-zero",link: "https://github.com/tal-tech/go-zero", + text: "go-zero",link: "https://github.com/zeromicro/go-zero", }, { text: "CDS",link: "https://github.com/tal-tech/cds", diff --git a/docs/README.md b/docs/README.md index 5201649b..887b3014 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,3 +1,4 @@ +# DEPRECATED: PLEASE MOVE TO https://go-zero.dev --- home: true heroImage: /logo.png diff --git a/docs/zero/TraceHandler.md b/docs/zero/TraceHandler.md index c094a918..e520d8f7 100644 --- a/docs/zero/TraceHandler.md +++ b/docs/zero/TraceHandler.md @@ -9,10 +9,10 @@ ## 代码结构 -- [spancontext](https://github.com/tal-tech/go-zero/blob/master/core/trace/spancontext.go):保存链路的上下文信息「traceid,spanid,或者是其他想要传递的内容」 -- [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):保存链路的上下文信息「traceid,spanid,或者是其他想要传递的内容」 +- [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` 中http,rpc中已经作为内置中间件集成。我们以 [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` 中http,rpc中已经作为内置中间件集成。我们以 [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) diff --git a/docs/zero/bloom.md b/docs/zero/bloom.md index 1e2eab26..07cbc252 100644 --- a/docs/zero/bloom.md +++ b/docs/zero/bloom.md @@ -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,简单使用方式如下 diff --git a/docs/zero/bookstore-en.md b/docs/zero/bookstore-en.md index 45108bd8..d08c671e 100644 --- a/docs/zero/bookstore-en.md +++ b/docs/zero/bookstore-en.md @@ -19,11 +19,11 @@ To build a well working microservice, we need lots of knowledges from different 3. logging, collects data and helps to backtrace problems 4. observability, no metrics, no optimization -For any point listed above, we need a long article to describe the theory and the implementation. But for us, the developers, it’s very difficult to understand all the concepts and make it happen in our systems. Although, we can use the frameworks that have been well served busy sites. [go-zero](https://github.com/tal-tech/go-zero) is born for this purpose, especially for cloud-native microservice systems. +For any point listed above, we need a long article to describe the theory and the implementation. But for us, the developers, it’s very difficult to understand all the concepts and make it happen in our systems. Although, we can use the frameworks that have been well served busy sites. [go-zero](https://github.com/zeromicro/go-zero) is born for this purpose, especially for cloud-native microservice systems. 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`. -Let’s 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, you’ll find that it’s so easy to write microservices! +Let’s 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, you’ll find that it’s so easy to write microservices! ## 1. What is a bookstore service diff --git a/docs/zero/bookstore.md b/docs/zero/bookstore.md index 0e3367fc..03773ae0 100644 --- a/docs/zero/bookstore.md +++ b/docs/zero/bookstore.md @@ -19,11 +19,11 @@ 3. 日志,用于数据收集和问题定位 4. 可观测性,没有度量就没有优化 -对于其中每一点,我们都需要用很长的篇幅来讲述其原理和实现,那么对我们后端开发者来说,要想把这些知识点都掌握并落实到业务系统里,难度是非常大的,不过我们可以依赖已经被大流量验证过的框架体系。[go-zero微服务框架](https://github.com/tal-tech/go-zero)就是为此而生。 +对于其中每一点,我们都需要用很长的篇幅来讲述其原理和实现,那么对我们后端开发者来说,要想把这些知识点都掌握并落实到业务系统里,难度是非常大的,不过我们可以依赖已经被大流量验证过的框架体系。[go-zero微服务框架](https://github.com/zeromicro/go-zero)就是为此而生。 另外,我们始终秉承**工具大于约定和文档**的理念。我们希望尽可能减少开发人员的心智负担,把精力都投入到产生业务价值的代码上,减少重复代码的编写,所以我们开发了`goctl`工具。 -下面我通过书店服务来演示通过[go-zero](https://github.com/tal-tech/go-zero)快速的创建微服务的流程,走完一遍,你就会发现:原来编写微服务如此简单! +下面我通过书店服务来演示通过[go-zero](https://github.com/zeromicro/go-zero)快速的创建微服务的流程,走完一遍,你就会发现:原来编写微服务如此简单! ## 1. 书店服务示例简介 diff --git a/docs/zero/executors.md b/docs/zero/executors.md index 58b8368d..ea1cb0dd 100644 --- a/docs/zero/executors.md +++ b/docs/zero/executors.md @@ -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)才出现,为什么会这么设计? diff --git a/docs/zero/goctl-model.md b/docs/zero/goctl-model.md index 44651abb..5fe717db 100644 --- a/docs/zero/goctl-model.md +++ b/docs/zero/goctl-model.md @@ -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`:略 diff --git a/docs/zero/goctl-overview.md b/docs/zero/goctl-overview.md index 1473a0e3..8665904f 100644 --- a/docs/zero/goctl-overview.md +++ b/docs/zero/goctl-overview.md @@ -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文件,然后将其添加到环境变量中。 # 校验 diff --git a/docs/zero/goctl-plugin.md b/docs/zero/goctl-plugin.md index 6ae42806..c0b9a413 100644 --- a/docs/zero/goctl-plugin.md +++ b/docs/zero/goctl-plugin.md @@ -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 diff --git a/docs/zero/goctl-rpc.md b/docs/zero/goctl-rpc.md index 0399b235..eee3764f 100644 --- a/docs/zero/goctl-rpc.md +++ b/docs/zero/goctl-rpc.md @@ -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] ``` diff --git a/docs/zero/periodlimit.md b/docs/zero/periodlimit.md index b6d87642..82d9ebc5 100644 --- a/docs/zero/periodlimit.md +++ b/docs/zero/periodlimit.md @@ -120,7 +120,7 @@ end ## 参考 -- [go-zero periodlimit](https://github.com/tal-tech/go-zero/blob/master/core/limit/periodlimit.go) +- [go-zero periodlimit](https://github.com/zeromicro/go-zero/blob/master/core/limit/periodlimit.go) - [分布式服务限流实战,已经为你排好坑了](https://www.infoq.cn/article/Qg2tX8fyw5Vt-f3HH673) diff --git a/docs/zero/streamapi-fx.md b/docs/zero/streamapi-fx.md index 25ba4f38..28b55cb1 100644 --- a/docs/zero/streamapi-fx.md +++ b/docs/zero/streamapi-fx.md @@ -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) diff --git a/docs/zero/timingWheel.md b/docs/zero/timingWheel.md index a1ef1933..9897e910 100644 --- a/docs/zero/timingWheel.md +++ b/docs/zero/timingWheel.md @@ -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) diff --git a/docs/zero/tokenlimit.md b/docs/zero/tokenlimit.md index 24746583..32609a00 100644 --- a/docs/zero/tokenlimit.md +++ b/docs/zero/tokenlimit.md @@ -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) diff --git "a/docs/zero/\345\210\233\345\273\272API\346\234\215\345\212\241.md" "b/docs/zero/\345\210\233\345\273\272API\346\234\215\345\212\241.md" index e238de39..4efae444 100644 --- "a/docs/zero/\345\210\233\345\273\272API\346\234\215\345\212\241.md" +++ "b/docs/zero/\345\210\233\345\273\272API\346\234\215\345\212\241.md" @@ -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) diff --git "a/docs/zero/\346\265\201\346\225\260\346\215\256\345\244\204\347\220\206\345\210\251\345\231\250.md" "b/docs/zero/\346\265\201\346\225\260\346\215\256\345\244\204\347\220\206\345\210\251\345\231\250.md" index d3b7d04c..af299497 100644 --- "a/docs/zero/\346\265\201\346\225\260\346\215\256\345\244\204\347\220\206\345\210\251\345\231\250.md" +++ "b/docs/zero/\346\265\201\346\225\260\346\215\256\345\244\204\347\220\206\345\210\251\345\231\250.md" @@ -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 diff --git "a/docs/zero/\351\231\204\345\275\2251.md" "b/docs/zero/\351\231\204\345\275\2251.md" index c9eb0b80..9c98ba0b 100644 --- "a/docs/zero/\351\231\204\345\275\2251.md" +++ "b/docs/zero/\351\231\204\345\275\2251.md" @@ -360,7 +360,7 @@ info( **语法定义** -> 由于其和golang相似,因此不做详细说明,具体语法定义请在[ApiParser.g4](https://github.com/tal-tech/go-zero/blob/master/tools/goctl/api/parser/g4/ApiParser.g4)中查看typeSpec定义。 +> 由于其和golang相似,因此不做详细说明,具体语法定义请在[ApiParser.g4](https://github.com/zeromicro/go-zero/blob/master/tools/goctl/api/parser/g4/ApiParser.g4)中查看typeSpec定义。 diff --git a/go-zero.dev/LANGS.md b/go-zero.dev/LANGS.md new file mode 100644 index 00000000..d9529b4c --- /dev/null +++ b/go-zero.dev/LANGS.md @@ -0,0 +1,2 @@ +* [English](en) +* [中文](cn) \ No newline at end of file diff --git a/go-zero.dev/README-EN.MD b/go-zero.dev/README-EN.MD new file mode 100644 index 00000000..fadf78a1 --- /dev/null +++ b/go-zero.dev/README-EN.MD @@ -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!!!* \ No newline at end of file diff --git a/go-zero.dev/README.MD b/go-zero.dev/README.MD new file mode 100644 index 00000000..4213a28a --- /dev/null +++ b/go-zero.dev/README.MD @@ -0,0 +1,3 @@ +# go-zero.dev +本目录主是官方文档 https://go-zero.dev 的源文档,修改后 pr 请 assign [anqiansong](https://github.com/anqiansong) +进行 review,切记! \ No newline at end of file diff --git a/go-zero.dev/book.json b/go-zero.dev/book.json new file mode 100644 index 00000000..caf0a655 --- /dev/null +++ b/go-zero.dev/book.json @@ -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" + ] + } + } +} diff --git a/go-zero.dev/cn/README.md b/go-zero.dev/cn/README.md new file mode 100644 index 00000000..477844d9 --- /dev/null +++ b/go-zero.dev/cn/README.md @@ -0,0 +1,218 @@ + + +# go-zero + +[![Go](https://github.com/zeromicro/go-zero/workflows/Go/badge.svg?branch=master)](https://github.com/zeromicro/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/zeromicro/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 描述,一键生成各端代码 +* 自动校验客户端请求参数合法性 +* 大量微服务治理和并发工具包 + +架构图 + +## 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` 插件 + + + + + + + + + + + + + + +
插件 用途
goctl-swagger 一键生成 apiswagger 文档
goctl-android 生成 java (android)http client 请求代码
goctl-go-compact 合并 api 里同一个 group 里的 handler 到一个 go 文件
+ +## 8. 微信公众号 + +`go-zero` 相关文章都会在 `微服务实践` 公众号整理呈现,欢迎扫码关注,也可以通过公众号私信我 👏 + +wechat + +## 9. 微信交流群 + +如果文档中未能覆盖的任何疑问,欢迎您在群里提出,我们会尽快答复。 + +您可以在群内提出使用中需要改进的地方,我们会考虑合理性并尽快修改。 + +如果您发现 ***bug*** 请及时提 ***issue***,我们会尽快确认并修改。 + +为了防止广告用户、识别技术同行,请 ***star*** 后加我时注明 **github** 当前 ***star*** 数,我再拉进 **go-zero** 群,感谢! + +加我之前有劳点一下 ***star***,一个小小的 ***star*** 是作者们回答海量问题的动力🤝 + +wechat diff --git a/go-zero.dev/cn/about-us.md b/go-zero.dev/cn/about-us.md new file mode 100644 index 00000000..1b248049 --- /dev/null +++ b/go-zero.dev/cn/about-us.md @@ -0,0 +1,20 @@ +# 关于我们 + +## go-zero +go-zero 是一个集成了各种工程实践的 web 和 rpc 框架。通过弹性设计保障了大并发服务端的稳定性,经受了充分的实战检验。 + +go-zero 包含极简的 API 定义和生成工具 goctl,可以根据定义的 api 文件一键生成 Go, iOS, Android, Kotlin, Dart, TypeScript, JavaScript 代码,并可直接运行。 + +## go-zero作者 +[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社区群 +社区群 diff --git a/go-zero.dev/cn/api-coding.md b/go-zero.dev/cn/api-coding.md new file mode 100644 index 00000000..53c91358 --- /dev/null +++ b/go-zero.dev/cn/api-coding.md @@ -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) \ No newline at end of file diff --git a/go-zero.dev/cn/api-config.md b/go-zero.dev/cn/api-config.md new file mode 100644 index 00000000..aa0aeb7f --- /dev/null +++ b/go-zero.dev/cn/api-config.md @@ -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-输出到console,file-输出到当前服务器(容器)文件,,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密码 +} +``` diff --git a/go-zero.dev/cn/api-dir.md b/go-zero.dev/cn/api-dir.md new file mode 100644 index 00000000..a1a0cd55 --- /dev/null +++ b/go-zero.dev/cn/api-dir.md @@ -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 +``` \ No newline at end of file diff --git a/go-zero.dev/cn/api-grammar.md b/go-zero.dev/cn/api-grammar.md new file mode 100644 index 00000000..2b133ed9 --- /dev/null +++ b/go-zero.dev/cn/api-grammar.md @@ -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`是否为一个合法值。 + +VALUE:key对应的值,可以为单行的除'\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 +) +``` + +eg3:key-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/zeromicro/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表 + + + + + + + + + + + + + +
tag key 描述 提供方有效范围 示例
json json序列化tag golang request、response json:"fooo"
path 路由path,如/foo/:id go-zero request path:"id"
form 标志请求体是一个form(POST方法时)或者一个query(GET方法时/search?name=keyword) go-zero request form:"name"
+* tag修饰符 + + 常见参数校验描述 + + + + + + + + + + + + + + + + +
tag key 描述 提供方 有效范围 示例
optional 定义当前字段为可选参数 go-zero request json:"name,optional"
options 定义当前字段的枚举值,多个以竖线|隔开 go-zero request json:"gender,options=male"
default 定义当前字段默认值 go-zero request json:"gender,default=male"
range 定义当前字段数值范围 go-zero request json:"age,range=[0:120]"
+ + > [!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`是必须是小写。 + +body:api请求体语法定义,必须要由()包裹的可选的ID值 + +replyBody:api响应体语法定义,必须由()包裹的struct、~~array(向前兼容处理,后续可能会废弃,强烈推荐以struct包裹,不要直接用array作为响应体)~~ + +kvLit: 同info key-value + +serviceName: 可以有多个'-'join的ID值 + +path:api请求路径,必须以'/'或者'/:'开头,切不能以'/'结尾,中间可包含ID或者多个以'-'join的ID字符串 + +**atServer关键key描述说明** + +修饰service时 + + + + + + + + + + + + + + +
key描述示例
jwt声明当前service下所有路由需要jwt鉴权,且会自动生成包含jwt逻辑的代码jwt: Auth
group声明当前service或者路由文件分组group: login
middleware声明当前service需要开启中间件middleware: AuthMiddleware
+ +修饰route时 + + + + + + + + +
key描述示例
handler声明一个handler-
+ +**正确语法示例** ✅ + +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的支持情况 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
语法块parent语法块DocComment
syntaxLitapi
kvLitinfoSpec
importLitimportSpec
typeLitapi
typeLittypeBlock
fieldtypeLit
key-valueatServer
atHandlerserviceRoute
routeserviceRoute
+ +以下为对应语法块解析后细带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 +} +``` diff --git a/go-zero.dev/cn/bloom.md b/go-zero.dev/cn/bloom.md new file mode 100644 index 00000000..aa62ef03 --- /dev/null +++ b/go-zero.dev/cn/bloom.md @@ -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 工具,在实际的项目中非常实用。用好工具对于提升服务性能和开发效率都有很大的帮助,希望本篇文章能给大家带来一些收获。 \ No newline at end of file diff --git a/doc/breaker-algorithms.md b/go-zero.dev/cn/breaker-algorithms.md similarity index 94% rename from doc/breaker-algorithms.md rename to go-zero.dev/cn/breaker-algorithms.md index 50062f7c..857acb26 100644 --- a/doc/breaker-algorithms.md +++ b/go-zero.dev/cn/breaker-algorithms.md @@ -2,7 +2,7 @@ 在微服务中服务间依赖非常常见,比如评论服务依赖审核服务而审核服务又依赖反垃圾服务,当评论服务调用审核服务时,审核服务又调用反垃圾服务,而这时反垃圾服务超时了,由于审核服务依赖反垃圾服务,反垃圾服务超时导致审核服务逻辑一直等待,而这个时候评论服务又在一直调用审核服务,审核服务就有可能因为堆积了大量请求而导致服务宕机 -call_chain +![call_chain](./resource/call_chain.png) 由此可见,在整个调用链中,中间的某一个环节出现异常就会引起上游调用服务出现一些列的问题,甚至导致整个调用链的服务都宕机,这是非常可怕的。因此一个服务作为调用方调用另一个服务时,为了防止被调用服务出现问题进而导致调用服务出现问题,所以调用服务需要进行自我保护,而保护的常用手段就是***熔断*** @@ -16,17 +16,17 @@ - 打开(Open):在该状态下,发起请求时会立即返回错误,一般会启动一个超时计时器,当计时器超时后,状态切换到半打开状态,也可以设置一个定时器,定期的探测服务是否恢复 - 半打开(Half-Open):在该状态下,允许应用程序一定数量的请求发往被调用服务,如果这些调用正常,那么可以认为被调用服务已经恢复正常,此时熔断器切换到关闭状态,同时需要重置计数。如果这部分仍有调用失败的情况,则认为被调用方仍然没有恢复,熔断器会切换到关闭状态,然后重置计数器,半打开状态能够有效防止正在恢复中的服务被突然大量请求再次打垮 -breaker_state +![breaker_state](./resource/breaker_state.png) 服务治理中引入熔断机制,使得系统更加稳定和有弹性,在系统从错误中恢复的时候提供稳定性,并且减少了错误对系统性能的影响,可以快速拒绝可能导致错误的服务调用,而不需要等待真正的错误返回 ### 熔断器引入 -上面介绍了熔断器的原理,在了解完原理后,你是否有思考我们如何引入熔断器呢?一种方案是在业务逻辑中可以加入熔断器,但显然是不够优雅也不够通用的,因此我们需要把熔断器集成在框架内,在[zRPC](https://github.com/tal-tech/go-zero/tree/master/zrpc)框架内就内置了熔断器 +上面介绍了熔断器的原理,在了解完原理后,你是否有思考我们如何引入熔断器呢?一种方案是在业务逻辑中可以加入熔断器,但显然是不够优雅也不够通用的,因此我们需要把熔断器集成在框架内,在[zRPC](https://github.com/zeromicro/go-zero/tree/master/zrpc)框架内就内置了熔断器 我们知道,熔断器主要是用来保护调用端,调用端在发起请求的时候需要先经过熔断器,而客户端拦截器正好兼具了这个这个功能,所以在zRPC框架内熔断器是实现在客户端拦截器内,拦截器的原理如下图: -interceptor +![interceptor](./resource/interceptor.png) 对应的代码为: @@ -52,7 +52,7 @@ zRPC中熔断器的实现参考了[Google Sre过载保护算法](https://landing 在正常情况下,这两个值是相等的,随着被调用方服务出现异常开始拒绝请求,请求接受数量(accepts)的值开始逐渐小于请求数量(requests),这个时候调用方可以继续发送请求,直到requests = K * accepts,一旦超过这个限制,熔断器就回打开,新的请求会在本地以一定的概率被抛弃直接返回错误,概率的计算公式如下: -client_rejection2 +![client_rejection2](./resource/client_rejection2.png) 通过修改算法中的K(倍值),可以调节熔断器的敏感度,当降低该倍值会使自适应熔断算法更敏感,当增加该倍值会使得自适应熔断算法降低敏感度,举例来说,假设将调用方的请求上限从 requests = 2 * acceptst 调整为 requests = 1.1 * accepts 那么就意味着调用方每十个请求之中就有一个请求会触发熔断 diff --git a/go-zero.dev/cn/buiness-cache.md b/go-zero.dev/cn/buiness-cache.md new file mode 100644 index 00000000..927666f9 --- /dev/null +++ b/go-zero.dev/cn/buiness-cache.md @@ -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/zeromicro/go-zero/blob/master/core/collection/cache.go) ,其是专门 + 用于内存管理. +* 采用biz redis,并设置合理的过期时间 + +# 总结 +以上两个场景可以包含大部分的多行记录缓存,对于多行记录查询量不大的场景,暂时没必要直接把biz redis放进去,可以先尝试让db来承担,开发人员可以根据持久层监控及服务 +监控来衡量时候需要引入biz。 diff --git a/go-zero.dev/cn/business-coding.md b/go-zero.dev/cn/business-coding.md new file mode 100644 index 00000000..66ff45c6 --- /dev/null +++ b/go-zero.dev/cn/business-coding.md @@ -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) \ No newline at end of file diff --git a/go-zero.dev/cn/business-dev.md b/go-zero.dev/cn/business-dev.md new file mode 100644 index 00000000..ce273c7a --- /dev/null +++ b/go-zero.dev/cn/business-dev.md @@ -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)章节过来,这份源码结构对你来说不是问题。 + +点击这里下载演示工程基础源码 + +## 演示工程说明 + +### 场景 +程序员小明需要借阅一本《西游记》,在没有线上图书管理系统的时候,他每天都要去图书馆前台咨询图书馆管理员, +* 小明:你好,请问今天《西游记》的图书还有吗? +* 管理员:没有了,明天再来看看吧。 + +过了一天,小明又来到图书馆,问: +* 小明:你好,请问今天《西游记》的图书还有吗? +* 管理员:没有了,你过两天再来看看吧。 + +就这样经过多次反复,小明也是徒劳无功,浪费大量时间在来回的路上,于是终于忍受不了落后的图书管理系统, +他决定自己亲手做一个图书查阅系统。 + +### 预期实现目标 +* 用户登录 + 依靠现有学生系统数据进行登录 +* 图书检索 + 根据图书关键字搜索图书,查询图书剩余数量。 + +### 系统分析 + +#### 服务拆分 +* 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','男'); +``` \ No newline at end of file diff --git a/go-zero.dev/cn/ci-cd.md b/go-zero.dev/cn/ci-cd.md new file mode 100644 index 00000000..089c5031 --- /dev/null +++ b/go-zero.dev/cn/ci-cd.md @@ -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来做简单的CI(Run 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/) \ No newline at end of file diff --git a/go-zero.dev/cn/coding-spec.md b/go-zero.dev/cn/coding-spec.md new file mode 100644 index 00000000..e0be8663 --- /dev/null +++ b/go-zero.dev/cn/coding-spec.md @@ -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 + } + ``` \ No newline at end of file diff --git a/doc/collection.md b/go-zero.dev/cn/collection.md similarity index 88% rename from doc/collection.md rename to go-zero.dev/cn/collection.md index 3fb6f7d4..e166e715 100644 --- a/doc/collection.md +++ b/go-zero.dev/cn/collection.md @@ -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) { diff --git a/go-zero.dev/cn/concept-introduction.md b/go-zero.dev/cn/concept-introduction.md new file mode 100644 index 00000000..f153a238 --- /dev/null +++ b/go-zero.dev/cn/concept-introduction.md @@ -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) \ No newline at end of file diff --git a/go-zero.dev/cn/config-introduction.md b/go-zero.dev/cn/config-introduction.md new file mode 100644 index 00000000..f8864f0f --- /dev/null +++ b/go-zero.dev/cn/config-introduction.md @@ -0,0 +1,4 @@ +# 配置介绍 +在正式使用go-zero之前,让我们先来了解一下go-zero中不同服务类型的配置定义,看看配置中每个字段分别有什么作用,本节将包含以下小节: +* [api配置](api-config.md) +* [rpc配置](rpc-config.md) \ No newline at end of file diff --git a/go-zero.dev/cn/datacenter.md b/go-zero.dev/cn/datacenter.md new file mode 100644 index 00000000..ce5e6a6e --- /dev/null +++ b/go-zero.dev/cn/datacenter.md @@ -0,0 +1,1053 @@ +# 我是如何用 go-zero 实现一个中台系统 + +> 作者:Jack Luo +> +> 原文连接:https://www.cnblogs.com/jackluo/p/14148518.html + +[TOC] + +最近发现golang社区里出了一个新星的微服务框架,来自好未来,光看这个名字,就很有奔头,之前,也只是玩过go-micro,其实真正的还没有在项目中运用过,只是觉得 微服务,grpc 这些很高大尚,还没有在项目中,真正的玩过,我看了一下官方提供的工具真的很好用,只需要定义好,舒适文件jia结构 都生成了,只需要关心业务,加上最近 有个投票的活动,加上最近这几年中台也比较火,所以决定玩一下, + +> 开源地址: [https://github.com/jackluo2012/datacenter](https://github.com/jackluo2012/datacenter) + +先聊聊中台架构思路吧: + +![](https://img2020.cnblogs.com/blog/203395/202012/203395-20201217094615171-335437652.jpg) + +中台的概念大概就是把一个一个的app 统一起来,反正我是这样理解的。 + +先聊用户服务吧,现在一个公司有很多的公众号、小程序、微信的、支付宝的,还有 xxx xxx,很多的平台,每次开发的时候,我们总是需要做用户登陆的服务,不停的复制代码,然后我们就在思考能不能有一套独立的用户服务,只需要告诉我你需要传个你要登陆的平台(比如微信),微信登陆,需要的是客户端返回给服务端一个code ,然后服务端拿着这个code去微信获取用户信息,反正大家都明白。 + +我们决定,将所有的信息弄到配置公共服务中去,里面再存微信、支付宝以及其它平台的appid、appkey、还有支付的appid、appkey,这样就写一套。 + +--- + +最后说说实现吧,整个就一个repo: + +- 网关,我们用的是: go-zero的Api服务 +- 其它它的是服务,我们就是用的go-zero的rpc服务 + +看下目录结构 + +![](https://img2020.cnblogs.com/blog/203395/202012/203395-20201209110504600-317546535.png) + +整个项目完成,我一个人操刀,写了1个来星期,我就实现了上面的中台系统。 + +## datacenter-api服务 + + +先看官方文档 [https://go-zero.dev/cn/](https://go-zero.dev/cn/) + +我们先把网关搭建起来: + +```shell +➜ blogs mkdir datacenter && cd datacenter +➜ datacenter go mod init datacenter +go: creating new go.mod: module datacenter +➜ datacenter +``` + +查看book目录: + + +``` +➜ datacenter tree +. +└── go.mod + +0 directories, 1 file +``` + + +### 创建api文件 + + +``` +➜ datacenter goctl api -o datacenter.api +Done. +➜ datacenter tree +. +├── datacenter.api +├── user.api #用户 +├── votes.api #投票 +├── search.api #搜索 +├── questions.api #问答 +└── go.mod +``` + + +### 定义api服务 + + +分别包含了上面的 **公共服务**,**用户服务**,**投票活动服务** + +datacenter.api的内容: + +``` +info( + title: "中台系统"// TODO: add title + desc: "中台系统"// TODO: add description + author: "jackluo" + email: "net.webjoy@gmail.com" +) + +import "user.api" +import "votes.api" +import "search.api" +import "questions.api" + +//获取 应用信息 +type Beid { + Beid int64 `json:"beid"` +} +type Token { + Token string `json:"token"` +} +type WxTicket { + Ticket string `json:"ticket"` +} +type Application { + Sname string `json:"Sname"` //名称 + Logo string `json:"logo"` // login + Isclose int64 `json:"isclose"` //是否关闭 + Fullwebsite string `json:"fullwebsite"` // 全站名称 +} +type SnsReq { + Beid + Ptyid int64 `json:"ptyid"` //对应平台 + BackUrl string `json:"back_url"` //登陆返回的地址 +} +type SnsResp { + Beid + Ptyid int64 `json:"ptyid"` //对应平台 + Appid string `json:"appid"` //sns 平台的id + Title string `json:"title"` //名称 + LoginUrl string `json:"login_url"` //微信登陆的地址 +} + +type WxShareResp { + Appid string `json:"appid"` + Timestamp int64 `json:"timestamp"` + Noncestr string `json:"noncestr"` + Signature string `json:"signature"` +} + +@server( + group: common +) +service datacenter-api { + @doc( + summary: "获取站点的信息" + ) + @handler appInfo + get /common/appinfo (Beid) returns (Application) + @doc( + summary: "获取站点的社交属性信息" + ) + @handler snsInfo + post /common/snsinfo (SnsReq) returns (SnsResp) + + //获取分享的 + @handler wxTicket + post /common/wx/ticket (SnsReq) returns (WxShareResp) + +} + +//上传需要登陆 +@server( + jwt: Auth + group: common +) +service datacenter-api { + @doc( + summary: "七牛上传凭证" + ) + @handler qiuniuToken + post /common/qiuniu/token (Beid) returns (Token) +} +``` +user.api内容 + +``` +//注册请求 +type RegisterReq struct { + // TODO: add members here and delete this comment + Mobile string `json:"mobile"` //基本一个手机号码就完事 + Password string `json:"password"` + Smscode string `json:"smscode"` //短信码 +} +//登陆请求 +type LoginReq struct{ + Mobile string `json:"mobile"` + Type int64 `json:"type"` //1.密码登陆,2.短信登陆 + Password string `json:"password"` +} +//微信登陆 +type WxLoginReq struct { + Beid int64 `json:"beid"` //应用id + Code string `json:"code"` //微信登陆密钥 + Ptyid int64 `json:"ptyid"` //对应平台 +} + +//返回用户信息 +type UserReply struct { + Auid int64 `json:"auid"` + Uid int64 `json:"uid"` + Beid int64 `json:"beid"` //应用id + Ptyid int64 `json:"ptyid"` //对应平台 + Username string `json:"username"` + Mobile string `json:"mobile"` + Nickname string `json:"nickname"` + Openid string `json:"openid"` + Avator string `json:"avator"` + JwtToken +} +//返回APPUser +type AppUser struct{ + Uid int64 `json:"uid"` + Auid int64 `json:"auid"` + Beid int64 `json:"beid"` //应用id + Ptyid int64 `json:"ptyid"` //对应平台 + Nickname string `json:"nickname"` + Openid string `json:"openid"` + Avator string `json:"avator"` +} + +type LoginAppUser struct{ + Uid int64 `json:"uid"` + Auid int64 `json:"auid"` + Beid int64 `json:"beid"` //应用id + Ptyid int64 `json:"ptyid"` //对应平台 + Nickname string `json:"nickname"` + Openid string `json:"openid"` + Avator string `json:"avator"` + JwtToken +} + +type JwtToken struct { + AccessToken string `json:"access_token,omitempty"` + AccessExpire int64 `json:"access_expire,omitempty"` + RefreshAfter int64 `json:"refresh_after,omitempty"` +} + +type UserReq struct{ + Auid int64 `json:"auid"` + Uid int64 `json:"uid"` + Beid int64 `json:"beid"` //应用id + Ptyid int64 `json:"ptyid"` //对应平台 +} + +type Request { + Name string `path:"name,options=you|me"` +} +type Response { + Message string `json:"message"` +} + + +@server( + group: user +) +service datacenter-api { + @handler ping + post /user/ping () + + @handler register + post /user/register (RegisterReq) returns (UserReply) + + @handler login + post /user/login (LoginReq) returns (UserReply) + + @handler wxlogin + post /user/wx/login (WxLoginReq) returns (LoginAppUser) + + @handler code2Session + get /user/wx/login () returns (LoginAppUser) +} +@server( + jwt: Auth + group: user + middleware: Usercheck +) +service datacenter-api { + @handler userInfo + get /user/dc/info (UserReq) returns (UserReply) +} +``` +votes.api 投票内容 +``` +// 投票活动api + + +type Actid struct { + Actid int64 `json:"actid"` //活动id +} + +type VoteReq struct { + Aeid int64 `json:"aeid"` // 作品id + Actid +} +type VoteResp struct { + VoteReq + Votecount int64 `json:"votecount"` //投票票数 + Viewcount int64 `json:"viewcount"` //浏览数 +} + + +// 活动返回的参数 + +type ActivityResp struct { + Actid int64 `json:"actid"` + Title string `json:"title"` //活动名称 + Descr string `json:"descr"` //活动描述 + StartDate int64 `json:"start_date"` //活动时间 + EnrollDate int64 `json:"enroll_date"` //投票时间 + EndDate int64 `json:"end_date"` //活动结束时间 + Votecount int64 `json:"votecount"` //当前活动的总票数 + Viewcount int64 `json:"viewcount"` //当前活动的总浏览数 + Type int64 `json:"type"` //投票方式 + Num int64 `json:"num"` //投票几票 +} +//报名 + + +type EnrollReq struct { + Actid + Name string `json:"name"` // 名称 + Address string `json:"address"` //地址 + Images []string `json:"images"` //作品图片 + Descr string `json:"descr"` // 作品描述 +} +// 作品返回 + +type EnrollResp struct { + Actid + Aeid int64 `json:"aeid"` // 作品id + Name string `json:"name"` // 名称 + Address string `json:"address"` //地址 + Images []string `json:"images"` //作品图片 + Descr string `json:"descr"` // 作品描述 + Votecount int64 `json:"votecount"` //当前活动的总票数 + Viewcount int64 `json:"viewcount"` //当前活动的总浏览数 + +} + + +@server( + group: votes +) +service datacenter-api { + @doc( + summary: "获取活动的信息" + ) + @handler activityInfo + get /votes/activity/info (Actid) returns (ActivityResp) + @doc( + summary: "活动访问+1" + ) + @handler activityIcrView + get /votes/activity/view (Actid) returns (ActivityResp) + @doc( + summary: "获取报名的投票作品信息" + ) + @handler enrollInfo + get /votes/enroll/info (VoteReq) returns (EnrollResp) + @doc( + summary: "获取报名的投票作品列表" + ) + @handler enrollLists + get /votes/enroll/lists (Actid) returns(EnrollResp) +} + +@server( + jwt: Auth + group: votes + middleware: Usercheck +) +service datacenter-api { + @doc( + summary: "投票" + ) + @handler vote + post /votes/vote (VoteReq) returns (VoteResp) + @handler enroll + post /votes/enroll (EnrollReq) returns (EnrollResp) +} + +``` + +questions.api 问答内容: +``` +// 问答 抽奖 开始 +@server( + group: questions +) +service datacenter-api { + @doc( + summary: "获取活动的信息" + ) + @handler activitiesInfo + get /questions/activities/info (Actid) returns (ActivityResp) + @doc( + summary: "获取奖品信息" + ) + @handler awardInfo + get /questions/award/info (Actid) returns (ActivityResp) + + @handler awardList + get /questions/award/list (Actid) returns (ActivityResp) + +} +type AnswerReq struct { + ActivityId int64 `json:"actid"` + Answers string `json:"answers"` + Score string `json:"score"` +} +type QuestionsAwardReq struct { + ActivityId int64 `json:"actid"` + AnswerId int64 `json:"answerid"` +} +type AnswerResp struct { + Answers string `json:"answers"` + Score string `json:"score"` +} +type AwardConvertReq struct { + UserName string `json:"username"` + Phone string `json:"phone"` + LotteryId int64 `json:"lotteryid"` +} + + +@server( + jwt: Auth + group: questions + middleware: Usercheck +) +service datacenter-api { + @doc( + summary: "获取题目" + ) + @handler lists + get /questions/lists (VoteReq) returns (AnswerResp) + @doc( + summary: "提交答案" + ) + @handler change + post /questions/change (AnswerReq) returns (VoteResp) + + @doc( + summary: "获取分数" + ) + @handler grade + get /questions/grade (VoteReq) returns (VoteResp) + + @doc( + summary: "开始转盘" + ) + @handler turntable + post /questions/lottery/turntable (EnrollReq) returns (EnrollResp) + @doc( + summary: "填写中奖信息人" + ) + @handler lottery + post /questions/lottery/convert (AwardConvertReq) returns (EnrollResp) +} + + +// 问答 抽奖 结束 +``` +search.api 搜索 +``` + + +type SearchReq struct { + Keyword string `json:"keyword"` + Page string `json:"page"` + Size string `json:"size"` +} +type SearchResp struct { + Data []ArticleReq `json:"data"` +} + +type ArticleReq struct{ + NewsId string `json:"NewsId"` + NewsTitle string `json:"NewsTitle"` + ImageUrl string `json:"ImageUrl"` +} + + +@server( + group: search + middleware: Admincheck +) +service datacenter-api { + @doc( + summary: "搜索" + ) + @handler article + get /search/article (SearchReq) returns (SearchResp) + @handler articleInit + get /search/articel/init (SearchReq) returns (SearchResp) + @handler articleStore + post /search/articel/store (ArticleReq) returns (ArticleReq) +} + +``` + +上面基本上写就写的API及文档的思路 + + +### 生成datacenter api服务 + + +``` +➜ datacenter goctl api go -api datacenter.api -dir . +Done. +➜ datacenter treer +. +├── datacenter.api +├── etc +│ └── datacenter-api.yaml +├── go.mod +├── internal +│ ├── config +│ │ └── config.go +│ ├── handler +│ │ ├── common +│ │ │ ├── appinfohandler.go +│ │ │ ├── qiuniutokenhandler.go +│ │ │ ├── snsinfohandler.go +│ │ │ ├── votesverificationhandler.go +│ │ │ └── wxtickethandler.go +│ │ ├── routes.go +│ │ ├── user +│ │ │ ├── code2sessionhandler.go +│ │ │ ├── loginhandler.go +│ │ │ ├── pinghandler.go +│ │ │ ├── registerhandler.go +│ │ │ ├── userinfohandler.go +│ │ │ └── wxloginhandler.go +│ │ └── votes +│ │ ├── activityicrviewhandler.go +│ │ ├── activityinfohandler.go +│ │ ├── enrollhandler.go +│ │ ├── enrollinfohandler.go +│ │ ├── enrolllistshandler.go +│ │ └── votehandler.go +│ ├── logic +│ │ ├── common +│ │ │ ├── appinfologic.go +│ │ │ ├── qiuniutokenlogic.go +│ │ │ ├── snsinfologic.go +│ │ │ ├── votesverificationlogic.go +│ │ │ └── wxticketlogic.go +│ │ ├── user +│ │ │ ├── code2sessionlogic.go +│ │ │ ├── loginlogic.go +│ │ │ ├── pinglogic.go +│ │ │ ├── registerlogic.go +│ │ │ ├── userinfologic.go +│ │ │ └── wxloginlogic.go +│ │ └── votes +│ │ ├── activityicrviewlogic.go +│ │ ├── activityinfologic.go +│ │ ├── enrollinfologic.go +│ │ ├── enrolllistslogic.go +│ │ ├── enrolllogic.go +│ │ └── votelogic.go +│ ├── middleware +│ │ └── usercheckmiddleware.go +│ ├── svc +│ │ └── servicecontext.go +│ └── types +│ └── types.go +└── datacenter.go + +14 directories, 43 files +``` + + +我们打开 `etc/datacenter-api.yaml` 把必要的配置信息加上 + + +```yaml +Name: datacenter-api +Log: + Mode: console +Host: 0.0.0.0 +Port: 8857 +Auth: + AccessSecret: 你的jwtwon Secret + AccessExpire: 86400 +CacheRedis: +- Host: 127.0.0.1:6379 + Pass: 密码 + Type: node +UserRpc: + Etcd: + Hosts: + - 127.0.0.1:2379 + Key: user.rpc +CommonRpc: + Etcd: + Hosts: + - 127.0.0.1:2379 + Key: common.rpc +VotesRpc: + Etcd: + Hosts: + - 127.0.0.1:2379 + Key: votes.rpc +``` + + +上面的 `UserRpc`, `CommonRpc` ,还有 `VotesRpc` 这些我先写上,后面再来慢慢加。 + + +我们先来写 `CommonRpc` 服务。 + + +## CommonRpc服务 + + +### 新建项目目录 + + +``` +➜ datacenter mkdir -p common/rpc && cd common/rpc +``` + + +直接就新建在了,datacenter目录中,因为common 里面,可能以后会不只会提供rpc服务,可能还有api的服务,所以又加了rpc目录 + + +### goctl创建模板 + + +``` +➜ rpc goctl rpc template -o=common.proto +➜ rpc ls +common.proto +``` + + +往里面填入内容: + + +```protobufbuf +➜ rpc cat common.proto +syntax = "proto3"; + +option go_package = "common"; + +package common; + + +message BaseAppReq{ + int64 beid=1; +} + +message BaseAppResp{ + int64 beid=1; + string logo=2; + string sname=3; + int64 isclose=4; + string fullwebsite=5; +} + +// 请求的api +message AppConfigReq { + int64 beid=1; + int64 ptyid=2; +} + +// 返回的值 +message AppConfigResp { + int64 id=1; + int64 beid=2; + int64 ptyid=3; + string appid=4; + string appsecret=5; + string title=6; +} + +service Common { + rpc GetAppConfig(AppConfigReq) returns(AppConfigResp); + rpc GetBaseApp(BaseAppReq) returns(BaseAppResp); +} +``` + + +### gotcl生成rpc服务 + + +```bash +➜ rpc goctl rpc proto -src common.proto -dir . +protoc -I=/Users/jackluo/works/blogs/datacenter/common/rpc common.proto --go_out=plugins=grpc:/Users/jackluo/works/blogs/datacenter/common/rpc/common +Done. +``` + + +``` +➜ rpc tree +. +├── common +│ └── common.pb.go +├── common.go +├── common.proto +├── commonclient +│ └── common.go +├── etc +│ └── common.yaml +└── internal +├── config +│ └── config.go +├── logic +│ ├── getappconfiglogic.go +│ └── getbaseapplogic.go +├── server +│ └── commonserver.go +└── svc +└── servicecontext.go + +8 directories, 10 files +``` + + +基本上,就把所有的目录规范和结构的东西都生成了,就不用纠结项目目录了,怎么放了,怎么组织了。 + + +看一下,配置信息,里面可以写入mysql和其它redis的信息: + + +```yaml +Name: common.rpc +ListenOn: 127.0.0.1:8081 +Mysql: + DataSource: root:admin@tcp(127.0.0.1:3306)/datacenter?charset=utf8&parseTime=true&loc=Asia%2FShanghai +CacheRedis: +- Host: 127.0.0.1:6379 + Pass: + Type: node +Etcd: + Hosts: + - 127.0.0.1:2379 + Key: common.rpc +``` + + +我们再来加上数据库服务: + + +``` +➜ rpc cd .. +➜ common ls +rpc +➜ common pwd +/Users/jackluo/works/blogs/datacenter/common +➜ common goctl model mysql datasource -url="root:admin@tcp(127.0.0.1:3306)/datacenter" -table="base_app" -dir ./model -c +Done. +➜ common tree +. +├── model +│ ├── baseappmodel.go +│ └── vars.go +└── rpc + ├── common + │ └── common.pb.go + ├── common.go + ├── common.proto + ├── commonclient + │ └── common.go + ├── etc + │ └── common.yaml + └── internal + ├── config + │ └── config.go + ├── logic + │ ├── getappconfiglogic.go + │ └── getbaseapplogic.go + ├── server + │ └── commonserver.go + └── svc + └── servicecontext.go + +10 directories, 12 files +``` + + +这样基本的一个 `rpc` 就写完了,然后我们将rpc 和model 还有api串连起来,这个官方的文档已经很详细了,这里就只是贴一下代码: + + +```go +➜ common cat rpc/internal/config/config.go +package config + +import ( + "github.com/tal-tech/go-zero/core/stores/cache" + "github.com/tal-tech/go-zero/zrpc" +) + +type Config struct { + zrpc.RpcServerConf + Mysql struct { + DataSource string + } + CacheRedis cache.ClusterConf +} +``` + + +再在svc中修改: + + +```go +➜ common cat rpc/internal/svc/servicecontext.go +package svc + +import ( + "datacenter/common/model" + "datacenter/common/rpc/internal/config" + + "github.com/tal-tech/go-zero/core/stores/sqlx" +) + +type ServiceContext struct { + c config.Config + AppConfigModel model.AppConfigModel + BaseAppModel model.BaseAppModel +} + +func NewServiceContext(c config.Config) *ServiceContext { + conn := sqlx.NewMysql(c.Mysql.DataSource) + apm := model.NewAppConfigModel(conn, c.CacheRedis) + bam := model.NewBaseAppModel(conn, c.CacheRedis) + return &ServiceContext{ + c: c, + AppConfigModel: apm, + BaseAppModel: bam, + } +} +``` + + +上面的代码已经将 `rpc` 和 `model` 数据库关联起来了,我们现在再将 `rpc` 和 `api` 关联起来: + + +```go +➜ datacenter cat internal/config/config.go + +package config + +import ( + "github.com/tal-tech/go-zero/core/stores/cache" + "github.com/tal-tech/go-zero/rest" + "github.com/tal-tech/go-zero/zrpc" +) + +type Config struct { + rest.RestConf + + Auth struct { + AccessSecret string + AccessExpire int64 + } + UserRpc zrpc.RpcClientConf + CommonRpc zrpc.RpcClientConf + VotesRpc zrpc.RpcClientConf + + CacheRedis cache.ClusterConf +} +``` + + +加入 `svc` 服务中: + + +```go +➜ datacenter cat internal/svc/servicecontext.go +package svc + +import ( + "context" + "datacenter/common/rpc/commonclient" + "datacenter/internal/config" + "datacenter/internal/middleware" + "datacenter/shared" + "datacenter/user/rpc/userclient" + "datacenter/votes/rpc/votesclient" + "fmt" + "net/http" + "time" + + "github.com/tal-tech/go-zero/core/logx" + "github.com/tal-tech/go-zero/core/stores/cache" + "github.com/tal-tech/go-zero/core/stores/redis" + "github.com/tal-tech/go-zero/core/syncx" + "github.com/tal-tech/go-zero/rest" + "github.com/tal-tech/go-zero/zrpc" + "google.golang.org/grpc" +) + +type ServiceContext struct { + Config config.Config + GreetMiddleware1 rest.Middleware + GreetMiddleware2 rest.Middleware + Usercheck rest.Middleware + UserRpc userclient.User //用户 + CommonRpc commonclient.Common + VotesRpc votesclient.Votes + Cache cache.Cache + RedisConn *redis.Redis +} + +func timeInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { + stime := time.Now() + err := invoker(ctx, method, req, reply, cc, opts...) + if err != nil { + return err + } + + fmt.Printf("调用 %s 方法 耗时: %v\n", method, time.Now().Sub(stime)) + return nil +} +func NewServiceContext(c config.Config) *ServiceContext { + + ur := userclient.NewUser(zrpc.MustNewClient(c.UserRpc, zrpc.WithUnaryClientInterceptor(timeInterceptor))) + cr := commonclient.NewCommon(zrpc.MustNewClient(c.CommonRpc, zrpc.WithUnaryClientInterceptor(timeInterceptor))) + vr := votesclient.NewVotes(zrpc.MustNewClient(c.VotesRpc, zrpc.WithUnaryClientInterceptor(timeInterceptor))) + //缓存 + ca := cache.NewCache(c.CacheRedis, syncx.NewSharedCalls(), cache.NewCacheStat("dc"), shared.ErrNotFound) + rcon := redis.NewRedis(c.CacheRedis[0].Host, c.CacheRedis[0].Type, c.CacheRedis[0].Pass) + return &ServiceContext{ + Config: c, + GreetMiddleware1: greetMiddleware1, + GreetMiddleware2: greetMiddleware2, + Usercheck: middleware.NewUserCheckMiddleware().Handle, + UserRpc: ur, + CommonRpc: cr, + VotesRpc: vr, + Cache: ca, + RedisConn: rcon, + } +} +``` + + +这样基本上,我们就可以在 `logic` 的文件目录中调用了: + + +```go +cat internal/logic/common/appinfologic.go + +package logic + +import ( + "context" + + "datacenter/internal/svc" + "datacenter/internal/types" + "datacenter/shared" + + "datacenter/common/model" + "datacenter/common/rpc/common" + + "github.com/tal-tech/go-zero/core/logx" +) + +type AppInfoLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAppInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) AppInfoLogic { + return AppInfoLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AppInfoLogic) AppInfo(req types.Beid) (appconfig *common.BaseAppResp, err error) { + + //检查 缓存中是否有值 + err = l.svcCtx.Cache.GetCache(model.GetcacheBaseAppIdPrefix(req.Beid), appconfig) + if err != nil && err == shared.ErrNotFound { + appconfig, err = l.svcCtx.CommonRpc.GetBaseApp(l.ctx, &common.BaseAppReq{ + Beid: req.Beid, + }) + if err != nil { + return + } + err = l.svcCtx.Cache.SetCache(model.GetcacheBaseAppIdPrefix(req.Beid), appconfig) + } + + return +} +``` + + +这样,基本就连接起来了,其它基本上就不用改了,`UserRPC`, `VotesRPC` 类似,这里就不在写了。 + + +## 使用心得 + + +`go-zero` 的确香,因为它有一个 `goctl` 的工具,他可以自动的把代码结构全部的生成好,我们就不再去纠结,目录结构 ,怎么组织,没有个好几年的架构能力是不好实现的,有什么规范那些,并发,熔断,完全不用,考滤其它的,专心的实现业务就好,像微服务,还要有服务发现,一系列的东西,都不用关心,因为 `go-zero` 内部已经实现了。 + + +我写代码也写了有10多年了,之前一直用的 php,比较出名的就 laravel,thinkphp,基本上就是模块化的,像微服那些实现直来真的有成本,但是你用上go-zero,你就像调api接口一样简单的开发,其它什么服务发现,那些根本就不用关注了,只需要关注业务。 + + +一个好的语言,框架,他们的底层思维,永远都是效率高,不加班的思想,我相信go-zero会提高你和你团队或是公司的效率。go-zero的作者说,他们有个团队专门整理go-zero框架,目的也应该很明显,那就是提高,他们自己的开发效率,流程化,标准化,是提高工作效率的准则,像我们平时遇到了问题,或是遇到了bug,我第一个想到的不是怎么去解决我的bug,而是在想我的流程是不是有问题,我的哪个流程会导致bug,最后我相信 `go-zero` 能成为 **微服务开发** 的首选框架。 + + +最后说说遇到的坑吧: + + +- `grpc` + + + +`grpc` 本人第一次用,然后就遇到了,有些字符为空时,字段值不显示的问题: + + +通过 `grpc` 官方库中的 `jsonpb` 来实现,官方在它的设定中有一个结构体用来实现 `protoc buffer` 转换为JSON结构,并可以根据字段来配置转换的要求。 + + +- 跨域问题 + + + +`go-zero` 中设置了,感觉没有效果,大佬说通过nginx 设置,后面发现还是不行,最近强行弄到了一个域名下,后面有时间再解决。 + + +- `sqlx` + + + +`go-zero` 的 `sqlx` 问题,这个真的费了很长的时间: + + +> `time.Time` 这个数据结构,数据库中用的是 timestamp 这个 比如我的字段 是delete_at 默认数库设置的是null ,结果插入的时候,就报了 `Incorrect datetime value: '0000-00-00' for column 'deleted_at' at row 1"}` 这个错,查询的时候报 `deleted_at\": unsupported Scan, storing driver.Value type \u003cnil\u003e into type *time.Time"` +> 后面果断去掉了这个字段,字段上面加上 `.omitempty` 这个标签,好像也有用,`db:".omitempty"` + + + +其次就是这个 `Conversion from collation utf8_general_ci into utf8mb4_unicode_ci`,这个导致的大概原因是,现在都喜欢用emj表情了,mysql数据识别不了。 + + +- 数据连接 + + + +`mysql` 这边照样按照原始的方式,将配置文件修改编码格式,重新创建数据库,并且设置数据库编码为utf8mb4,排序规则为 `utf8mb4_unicode_ci`。 + + +**这样的话,所有的表还有string字段都是这个编码格式,如果不想所有的都是,可以单独设置,这个不是重点.因为在navicat上都好设置,手动点一下就行了**。 + + +重点来了:golang中使用的是 `github.com/go-sql-driver/mysql` 驱动,将连接 `mysql`的 `dsn`(因为我这使用的是gorm,所以dsn可能跟原生的格式不太一样,不过没关系, 只需要关注 `charset` 和 `collation` 就行了) +`root:password@/name?parseTime=True&loc=Local&charset=utf8` 修改为: +`root:password@/name?parseTime=True&loc=Local&charset=utf8mb4&collation=utf8mb4_unicode_ci` diff --git a/go-zero.dev/cn/dev-flow.md b/go-zero.dev/cn/dev-flow.md new file mode 100644 index 00000000..8ceac057 --- /dev/null +++ b/go-zero.dev/cn/dev-flow.md @@ -0,0 +1,25 @@ +# 开发流程 +这里的开发流程和我们实际业务开发流程不是一个概念,这里的定义局限于go-zero的使用,即代码层面的开发细节。 + +## 开发流程 +* goctl环境准备[1] +* 数据库设计 +* 业务开发 +* 新建工程 +* 创建服务目录 +* 创建服务类型(api/rpc/rmq/job/script) +* 编写api、proto文件 +* 代码生成 +* 生成数据库访问层代码model +* 配置config,yaml变更 +* 资源依赖填充(ServiceContext) +* 添加中间件 +* 业务代码填充 +* 错误处理 + +> [!TIP] +> [1] [goctl环境](concept-introduction.md) + +## 开发工具 +* Visual Studio Code +* Goland(推荐) \ No newline at end of file diff --git a/go-zero.dev/cn/dev-specification.md b/go-zero.dev/cn/dev-specification.md new file mode 100644 index 00000000..9bb4b8a6 --- /dev/null +++ b/go-zero.dev/cn/dev-specification.md @@ -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的诞生正是为了提高生产力, +因此这个开发原则我是非常认同的。 + diff --git a/go-zero.dev/cn/doc-contibute.md b/go-zero.dev/cn/doc-contibute.md new file mode 100644 index 00000000..ddb7937a --- /dev/null +++ b/go-zero.dev/cn/doc-contibute.md @@ -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) + diff --git a/go-zero.dev/cn/error-handle.md b/go-zero.dev/cn/error-handle.md new file mode 100644 index 00000000..bde3c298 --- /dev/null +++ b/go-zero.dev/cn/error-handle.md @@ -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":"用户名不存在"} + ``` diff --git a/go-zero.dev/cn/error.md b/go-zero.dev/cn/error.md new file mode 100644 index 00000000..6bd7df0d --- /dev/null +++ b/go-zero.dev/cn/error.md @@ -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是否已经添加到环境变量 \ No newline at end of file diff --git a/go-zero.dev/cn/executors.md b/go-zero.dev/cn/executors.md new file mode 100644 index 00000000..3ec8e5c6 --- /dev/null +++ b/go-zero.dev/cn/executors.md @@ -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 { + // 既没到maxTask,Flush() 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/zeromicro/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` 中还有很多实用的组件工具,用好工具对于提升服务性能和开发效率都有很大的帮助,希望本篇文章能给大家带来一些收获。 diff --git a/go-zero.dev/cn/extended-reading.md b/go-zero.dev/cn/extended-reading.md new file mode 100644 index 00000000..fd100652 --- /dev/null +++ b/go-zero.dev/cn/extended-reading.md @@ -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) diff --git a/go-zero.dev/cn/faq.md b/go-zero.dev/cn/faq.md new file mode 100644 index 00000000..79b50e7c --- /dev/null +++ b/go-zero.dev/cn/faq.md @@ -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使用了import,goctl命令需要怎么写。 + > `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写在这里。 diff --git a/go-zero.dev/cn/framework-design.md b/go-zero.dev/cn/framework-design.md new file mode 100644 index 00000000..e2f8a3fd --- /dev/null +++ b/go-zero.dev/cn/framework-design.md @@ -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) diff --git a/doc/fx.md b/go-zero.dev/cn/fx.md similarity index 96% rename from doc/fx.md rename to go-zero.dev/cn/fx.md index 0f3c3384..e10f2c07 100644 --- a/doc/fx.md +++ b/go-zero.dev/cn/fx.md @@ -8,7 +8,7 @@ ### 流处理工具fx -[gozero](https://github.com/tal-tech/go-zero)是一个功能完备的微服务框架,框架中内置了很多非常实用的工具,其中就包含流数据处理工具[fx](https://github.com/tal-tech/go-zero/tree/master/core/fx),下面我们通过一个简单的例子来认识下该工具: +[gozero](https://github.com/zeromicro/go-zero)是一个功能完备的微服务框架,框架中内置了很多非常实用的工具,其中就包含流数据处理工具[fx](https://github.com/zeromicro/go-zero/tree/master/core/fx),下面我们通过一个简单的例子来认识下该工具: ```go package main diff --git a/go-zero.dev/cn/go-queue.md b/go-zero.dev/cn/go-queue.md new file mode 100644 index 00000000..b84818a4 --- /dev/null +++ b/go-zero.dev/cn/go-queue.md @@ -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,使用场景主要是做消息队列 + + 我们主要说一下dq,kq使用也一样的,只是依赖底层不同,如果没使用过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`解决重复消费问题。 \ No newline at end of file diff --git a/go-zero.dev/cn/go-zero-design.md b/go-zero.dev/cn/go-zero-design.md new file mode 100644 index 00000000..61a2f056 --- /dev/null +++ b/go-zero.dev/cn/go-zero-design.md @@ -0,0 +1,12 @@ +# go-zero设计理念 + +对于微服务框架的设计,我们期望保障微服务稳定性的同时,也要特别注重研发效率。所以设计之初,我们就有如下一些准则: + +* 保持简单,第一原则 +* 弹性设计,面向故障编程 +* 工具大于约定和文档 +* 高可用 +* 高并发 +* 易扩展 +* 对业务开发友好,封装复杂度 +* 约束做一件事只有一种方式 \ No newline at end of file diff --git a/go-zero.dev/cn/go-zero-features.md b/go-zero.dev/cn/go-zero-features.md new file mode 100644 index 00000000..7a6d0080 --- /dev/null +++ b/go-zero.dev/cn/go-zero-features.md @@ -0,0 +1,21 @@ +# go-zero特性 + +go-zero 是一个集成了各种工程实践的包含 web 和 rpc 框架,有如下主要特点: + +* 强大的工具支持,尽可能少的代码编写 +* 极简的接口 +* 完全兼容 net/http +* 支持中间件,方便扩展 +* 高性能 +* 面向故障编程,弹性设计 +* 内建服务发现、负载均衡 +* 内建限流、熔断、降载,且自动触发,自动恢复 +* API 参数自动校验 +* 超时级联控制 +* 自动缓存控制 +* 链路跟踪、统计报警等 +* 高并发支撑,稳定保障了疫情期间每天的流量洪峰 + +如下图,我们从多个层面保障了整体服务的高可用: + +![弹性设计](https://gitee.com/kevwan/static/raw/master/doc/images/resilience.jpg) \ No newline at end of file diff --git a/go-zero.dev/cn/goctl-api.md b/go-zero.dev/cn/goctl-api.md new file mode 100644 index 00000000..66e31615 --- /dev/null +++ b/go-zero.dev/cn/goctl-api.md @@ -0,0 +1,69 @@ +# api命令 +goctl api是goctl中的核心模块之一,其可以通过.api文件一键快速生成一个api服务,如果仅仅是启动一个go-zero的api演示项目, +你甚至都不用编码,就可以完成一个api服务开发及正常运行。在传统的api项目中,我们要创建各级目录,编写结构体, +定义路由,添加logic文件,这一系列操作,如果按照一条协议的业务需求计算,整个编码下来大概需要5~6分钟才能真正进入业务逻辑的编写, +这还不考虑编写过程中可能产生的各种错误,而随着服务的增多,随着协议的增多,这部分准备工作的时间将成正比上升, +而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/zeromicro/go-zero/tree/master/tools/goctl/config/readme.md] +``` + +* --dir 代码输出目录 +* --api 指定api源文件 +* --style 指定生成代码文件的文件名称风格,详情见[文件名称命名style说明](https://github.com/zeromicro/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) \ No newline at end of file diff --git a/go-zero.dev/cn/goctl-commands.md b/go-zero.dev/cn/goctl-commands.md new file mode 100644 index 00000000..eb494125 --- /dev/null +++ b/go-zero.dev/cn/goctl-commands.md @@ -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 + 指定最大副本数 + diff --git a/go-zero.dev/cn/goctl-install.md b/go-zero.dev/cn/goctl-install.md new file mode 100644 index 00000000..86fc59c6 --- /dev/null +++ b/go-zero.dev/cn/goctl-install.md @@ -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 diff --git a/go-zero.dev/cn/goctl-model.md b/go-zero.dev/cn/goctl-model.md new file mode 100644 index 00000000..dd544a18 --- /dev/null +++ b/go-zero.dev/cn/goctl-model.md @@ -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/zeromicro/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/zeromicro/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外,其他的代码均属于业务型代码,这个我觉得开发人员根据业务需要进行编写更好。 + +# 类型转换规则 +| 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 | \ No newline at end of file diff --git a/go-zero.dev/cn/goctl-other.md b/go-zero.dev/cn/goctl-other.md new file mode 100644 index 00000000..438a0eb4 --- /dev/null +++ b/go-zero.dev/cn/goctl-other.md @@ -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 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) diff --git a/go-zero.dev/cn/goctl-plugin.md b/go-zero.dev/cn/goctl-plugin.md new file mode 100644 index 00000000..2ece55c6 --- /dev/null +++ b/go-zero.dev/cn/goctl-plugin.md @@ -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) \ No newline at end of file diff --git a/doc/goctl-rpc.md b/go-zero.dev/cn/goctl-rpc.md similarity index 64% rename from doc/goctl-rpc.md rename to go-zero.dev/cn/goctl-rpc.md index ba546f55..44bc622e 100644 --- a/doc/goctl-rpc.md +++ b/go-zero.dev/cn/goctl-rpc.md @@ -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一键生成常见问题解决,见 常见问题解决 +rpc一键生成常见问题解决,见[常见错误处理](error.md) ### 方式二:通过指定proto生成rpc服务 @@ -79,11 +79,13 @@ rpc一键生成常见问题解决,见 常见问 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一键生成常见问题解决,见 常见问 * 生成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/zeromicro/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/zeromicro/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.2(see [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) diff --git a/go-zero.dev/cn/goctl.md b/go-zero.dev/cn/goctl.md new file mode 100644 index 00000000..411956af --- /dev/null +++ b/go-zero.dev/cn/goctl.md @@ -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 +``` + +版本号说明 +* version:goctl 版本号 +* 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的环境变量中。 diff --git a/go-zero.dev/cn/golang-install.md b/go-zero.dev/cn/golang-install.md new file mode 100644 index 00000000..4b2b7e22 --- /dev/null +++ b/go-zero.dev/cn/golang-install.md @@ -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/) diff --git a/go-zero.dev/cn/gomod-config.md b/go-zero.dev/cn/gomod-config.md new file mode 100644 index 00000000..5f6a5c5d --- /dev/null +++ b/go-zero.dev/cn/gomod-config.md @@ -0,0 +1,37 @@ +# Go Module设置 + +## Go Module介绍 +> Modules are how Go manages dependencies.[1] + +即Go Module是Golang管理依赖性的方式,像Java中的Maven,Android中的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) \ No newline at end of file diff --git a/go-zero.dev/cn/goreading.md b/go-zero.dev/cn/goreading.md new file mode 100644 index 00000000..275b3908 --- /dev/null +++ b/go-zero.dev/cn/goreading.md @@ -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) \ No newline at end of file diff --git a/go-zero.dev/cn/gotalk.md b/go-zero.dev/cn/gotalk.md new file mode 100644 index 00000000..5746001a --- /dev/null +++ b/go-zero.dev/cn/gotalk.md @@ -0,0 +1,3 @@ +# Go开源说 + +* [Go 开源说第四期 - Go-Zero](https://www.bilibili.com/video/BV1Jy4y127Xu) \ No newline at end of file diff --git a/go-zero.dev/cn/intellij.md b/go-zero.dev/cn/intellij.md new file mode 100644 index 00000000..0835d53f --- /dev/null +++ b/go-zero.dev/cn/intellij.md @@ -0,0 +1,114 @@ +# intellij插件 + +## Go-Zero Plugin + +[go-zero](https://github.com/zeromicro/go-zero) +[license](https://github.com/zeromicro/goctl-intellij/blob/main/LICENSE) +[release](https://github.com/zeromicro/goctl-intellij/releases) +[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) + + diff --git a/go-zero.dev/cn/join-us.md b/go-zero.dev/cn/join-us.md new file mode 100644 index 00000000..acebb0da --- /dev/null +++ b/go-zero.dev/cn/join-us.md @@ -0,0 +1,53 @@ +# 加入我们 + +## 概要 +go-zero + +[go-zero](https://github.com/zeromicro/go-zero) 是一个基于[MIT License](https://github.com/zeromicro/go-zero/blob/master/LICENSE) 的开源项目,大家在使用中发现bug,有新的特性等,均可以参与到go-zero的贡献中来,我们非常欢迎大家的积极参与,也会最快响应大家提出的各种问题,pr等。 + +## 贡献形式 +* [Pull Request](https://github.com/zeromicro/go-zero/pulls) +* [Issue](https://github.com/zeromicro/go-zero/issues) + +## 贡献须知 +go-zero 的Pull request中的代码需要满足一定规范 +* 命名规范,请阅读[命名规范](naming-spec.md) +* 以英文注释为主 +* pr时备注好功能特性,描述需要清晰,简洁 +* 增加单元测试覆盖率达80%+ + +## 贡献代码(pr) +* 进入[go-zero](https://github.com/zeromicro/go-zero) 项目,fork一份[go-zero](https://github.com/zeromicro/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/zeromicro/go-zero) 的[Pull requests](https://github.com/zeromicro/go-zero/pulls) 查看,应该有自己提交的记录,名称为你的开发时的分支名称 + + ![pr record](./resource/pr_record.png) + +## Issue +在我们的社区中,有很多伙伴会积极的反馈一些go-zero使用过程中遇到的问题,由于社区人数较多,我们虽然会实时的关注社区动态,但大家问题反馈过来都是随机的,当我们团队还在解决某一个伙伴提出的问题时,另外的问题也反馈上来,可能会导致团队会很容易忽略掉,为了能够一一的解决大家的问题,我们强烈建议大家通过issue的方式来反馈问题,包括但不限于bug,期望的新功能特性等,我们在实现某一个新特性时也会在issue中体现,大家在这里也能够在这里获取到go-zero的最新动向,也欢迎大家来积极的参与讨论。 + +### 怎么提Issue +* 点击[这里](https://github.com/zeromicro/go-zero/issues) 进入go-zero的Issue页面或者直接访问[https://github.com/zeromicro/go-zero/issues](https://github.com/zeromicro/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) \ No newline at end of file diff --git a/go-zero.dev/cn/jwt.md b/go-zero.dev/cn/jwt.md new file mode 100644 index 00000000..cdb20a01 --- /dev/null +++ b/go-zero.dev/cn/jwt.md @@ -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值。 +> +> $AccessExpire:jwt 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) diff --git a/doc/keywords.md b/go-zero.dev/cn/keywords.md similarity index 100% rename from doc/keywords.md rename to go-zero.dev/cn/keywords.md diff --git a/go-zero.dev/cn/learning-resource.md b/go-zero.dev/cn/learning-resource.md new file mode 100644 index 00000000..1bd06759 --- /dev/null +++ b/go-zero.dev/cn/learning-resource.md @@ -0,0 +1,5 @@ +# 学习资源 +这里将不定期更新go-zero的最新学习资源通道,目前包含通道有: +* [公众号](wechat.md) +* [Go夜读](goreading.md) +* [Go开源说](gotalk.md) \ No newline at end of file diff --git a/doc/loadshedding.md b/go-zero.dev/cn/loadshedding.md similarity index 100% rename from doc/loadshedding.md rename to go-zero.dev/cn/loadshedding.md diff --git a/go-zero.dev/cn/log-collection.md b/go-zero.dev/cn/log-collection.md new file mode 100644 index 00000000..f5271947 --- /dev/null +++ b/go-zero.dev/cn/log-collection.md @@ -0,0 +1,139 @@ +# 日志收集 +为了保证业务稳定运行,预测服务不健康风险,日志的收集可以帮助我们很好的观察当前服务的健康状况, +在传统业务开发中,机器部署还不是很多时,我们一般都是直接登录服务器进行日志查看、调试,但随着业务的增大,服务的不断拆分, +服务的维护成本也会随之变得越来越复杂,在分布式系统中,服务器机子增多,服务分布在不同的服务器上,当遇到问题时, +我们不能使用传统做法,登录到服务器进行日志排查和调试,这个复杂度可想而知。 +![log-flow](./resource/log-flow.png) + +> [!TIP] +> 如果是一个简单的单体服务系统或者服务过于小不建议直接使用,否则会适得其反。 + +## 准备工作 +* kafka +* elasticsearch +* kibana +* filebeat、Log-Pilot(k8s) +* 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) diff --git a/go-zero.dev/cn/logx.md b/go-zero.dev/cn/logx.md new file mode 100644 index 00000000..dff24e38 --- /dev/null +++ b/go-zero.dev/cn/logx.md @@ -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种,一种文件输出,一种控制台输出。推荐方式,当采用 k8s,docker 等部署方式的时候,可以将日志输出到控制台,使用日志收集器收集导入至 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 毫秒。 diff --git a/doc/mapping.md b/go-zero.dev/cn/mapping.md similarity index 96% rename from doc/mapping.md rename to go-zero.dev/cn/mapping.md index 9e49340f..6aafa233 100644 --- a/doc/mapping.md +++ b/go-zero.dev/cn/mapping.md @@ -112,7 +112,7 @@ service user { * `sex`:必填,取值为`male`或`female` * `avatar`:选填,默认为`default.png` -更多详情参见[unmarshaler_test.go](https://github.com/tal-tech/go-zero/blob/master/core/mapping/unmarshaler_test.go) +更多详情参见[unmarshaler_test.go](https://github.com/zeromicro/go-zero/blob/master/core/mapping/unmarshaler_test.go) ## 2. http api返回体的序列化 diff --git a/doc/mapreduce.md b/go-zero.dev/cn/mapreduce.md similarity index 95% rename from doc/mapreduce.md rename to go-zero.dev/cn/mapreduce.md index c87523f4..374e0a02 100644 --- a/doc/mapreduce.md +++ b/go-zero.dev/cn/mapreduce.md @@ -4,13 +4,13 @@ 那么通过什么手段来优化呢?我们首先想到的是通过并发来的方式来处理依赖,这样就能降低整个依赖的耗时,Go基础库中为我们提供了 [WaitGroup](https://golang.org/pkg/sync/#WaitGroup) 工具用来进行并发控制,但实际业务场景中多个依赖如果有一个出错我们期望能立即返回而不是等所有依赖都执行完再返回结果,而且WaitGroup中对变量的赋值往往需要加锁,每个依赖函数都需要添加Add和Done对于新手来说比较容易出错 -基于以上的背景,go-zero框架中为我们提供了并发处理工具[MapReduce](https://github.com/tal-tech/go-zero/blob/master/core/mr/mapreduce.go),该工具开箱即用,不需要做什么初始化,我们通过下图看下使用MapReduce和没使用的耗时对比: +基于以上的背景,go-zero框架中为我们提供了并发处理工具[MapReduce](https://github.com/zeromicro/go-zero/blob/master/core/mr/mapreduce.go),该工具开箱即用,不需要做什么初始化,我们通过下图看下使用MapReduce和没使用的耗时对比: ![依赖耗时对比](https://gitee.com/kevwan/static/raw/master/doc/images/mr_time.png) 相同的依赖,串行处理的话需要200ms,使用MapReduce后的耗时等于所有依赖中最大的耗时为100ms,可见MapReduce可以大大降低服务耗时,而且随着依赖的增加效果就会越明显,减少处理耗时的同时并不会增加服务器压力 -## 并发处理工具[MapReduce](https://github.com/tal-tech/go-zero/tree/master/core/mr) +## 并发处理工具[MapReduce](https://github.com/zeromicro/go-zero/tree/master/core/mr) [MapReduce](https://zh.wikipedia.org/wiki/MapReduce)是Google提出的一个软件架构,用于大规模数据集的并行运算,go-zero中的MapReduce工具正是借鉴了这种架构思想 diff --git a/doc/metric.md b/go-zero.dev/cn/metric.md similarity index 95% rename from doc/metric.md rename to go-zero.dev/cn/metric.md index 63cd35e5..7bb861d8 100644 --- a/doc/metric.md +++ b/go-zero.dev/cn/metric.md @@ -14,7 +14,7 @@ Prometheus Server直接从监控目标中或者间接通过推送网关来拉取 ## go-zero基于prometheus的服务指标监控 -[go-zero](https://github.com/tal-tech/go-zero) 框架中集成了基于prometheus的服务指标监控,下面我们通过go-zero官方的示例[shorturl](https://github.com/tal-tech/go-zero/blob/master/doc/shorturl.md)来演示是如何对服务指标进行收集监控的: +[go-zero](https://github.com/zeromicro/go-zero) 框架中集成了基于prometheus的服务指标监控,下面我们通过go-zero官方的示例[shorturl](https://github.com/zeromicro/go-zero/blob/master/doc/shorturl.md)来演示是如何对服务指标进行收集监控的: - 第一步需要先安装Prometheus,安装步骤请参考[官方文档](https://prometheus.io/) - go-zero默认不开启prometheus监控,开启方式很简单,只需要在shorturl-api.yaml文件中增加配置如下,其中Host为Prometheus Server地址为必填配置,Port端口不填默认9091,Path为用来拉取指标的路径默认为/metrics diff --git a/go-zero.dev/cn/micro-service.md b/go-zero.dev/cn/micro-service.md new file mode 100644 index 00000000..03ee4a65 --- /dev/null +++ b/go-zero.dev/cn/micro-service.md @@ -0,0 +1,309 @@ +# 微服务 + +在上一篇我们已经演示了怎样快速创建一个单体服务,接下来我们来演示一下如何快速创建微服务, +在本小节中,api部分其实和单体服务的创建逻辑是一样的,只是在单体服务中没有服务间的通讯而已, +且微服务中api服务会多一些rpc调用的配置。 + +## 前言 +本小节将以一个`订单服务`调用`用户服务`来简单演示一下,演示代码仅传递思路,其中有些环节不会一一列举。 + +## 情景提要 +假设我们在开发一个商城项目,而开发者小明负责用户模块(user)和订单模块(order)的开发,我们姑且将这两个模块拆分成两个微服务① + +> [!NOTE] +> ①:微服务的拆分也是一门学问,这里我们就不讨论怎么去拆分微服务的细节了。 + +## 演示功能目标 +* 订单服务(order)提供一个查询接口 +* 用户服务(user)提供一个方法供订单服务获取用户信息 + +## 服务设计分析 +根据情景提要我们可以得知,订单是直接面向用户,通过http协议访问数据,而订单内部需要获取用户的一些基础数据,既然我们的服务是采用微服务的架构设计, +那么两个服务(user,order)就必须要进行数据交换,服务间的数据交换即服务间的通讯,到了这里,采用合理的通讯协议也是一个开发人员需要 +考虑的事情,可以通过http,rpc等方式来进行通讯,这里我们选择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生成,goctl,goctl环境等怎么使用和安装,快速入门中不作详细概述,我们后续都会有详细的文档进行描述,你也可以点击下文的【猜你想看】快速跳转的对应文档查看。 + +# 源码 +[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) diff --git a/go-zero.dev/cn/middleware.md b/go-zero.dev/cn/middleware.md new file mode 100644 index 00000000..87ab4cb3 --- /dev/null +++ b/go-zero.dev/cn/middleware.md @@ -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"} +``` \ No newline at end of file diff --git a/go-zero.dev/cn/model-gen.md b/go-zero.dev/cn/model-gen.md new file mode 100644 index 00000000..1cd605ca --- /dev/null +++ b/go-zero.dev/cn/model-gen.md @@ -0,0 +1,55 @@ +# model生成 +首先,下载好[演示工程](https://go-zero.dev/cn/resource/book.zip) 后,我们以user的model来进行代码生成演示。 + +## 前言 +model是服务访问持久化数据层的桥梁,业务的持久化数据常存在于mysql,mongo等数据库中,我们都知道,对于一个数据库的操作莫过于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) diff --git a/go-zero.dev/cn/monolithic-service.md b/go-zero.dev/cn/monolithic-service.md new file mode 100644 index 00000000..ac31ef7d --- /dev/null +++ b/go-zero.dev/cn/monolithic-service.md @@ -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) + + + diff --git a/go-zero.dev/cn/mysql.md b/go-zero.dev/cn/mysql.md new file mode 100644 index 00000000..5e5bb120 --- /dev/null +++ b/go-zero.dev/cn/mysql.md @@ -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()` 都会自动回滚事务。 diff --git a/go-zero.dev/cn/naming-spec.md b/go-zero.dev/cn/naming-spec.md new file mode 100644 index 00000000..69773f6c --- /dev/null +++ b/go-zero.dev/cn/naming-spec.md @@ -0,0 +1,49 @@ +# 命名规范 +在任何语言开发中,都有其语言领域的一些命名规范,好的命名可以: +* 降低代码阅读成本 +* 降低维护难度 +* 降低代码复杂度 + +## 规范建议 +在我们实际开发中,有很多开发人可能是由某一语言转到另外一个语言领域,在转到另外一门语言后, +我们都会保留着对旧语言的编程习惯,在这里,我建议的是,虽然不同语言之前的某些规范可能是相通的, +但是我们最好能够按照官方的一些demo来熟悉是渐渐适应当前语言的编程规范,而不是直接将原来语言的编程规范也随之迁移过来。 + +## 命名准则 +* 当变量名称在定义和最后一次使用之间的距离很短时,简短的名称看起来会更好。 +* 变量命名应尽量描述其内容,而不是类型 +* 常量命名应尽量描述其值,而不是如何使用这个值 +* 在遇到for,if等循环或分支时,推荐单个字母命名来标识参数和返回值 +* 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) diff --git a/go-zero.dev/cn/online-exchange.md b/go-zero.dev/cn/online-exchange.md new file mode 100644 index 00000000..a7f90bc9 --- /dev/null +++ b/go-zero.dev/cn/online-exchange.md @@ -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集群,线上最大几十个集群为同一个服务提供缓存服务 + - 无缝扩缩容 + - 不存在没有过期时间的缓存,避免大量不常使用的数据占用资源,默认一周 + - 缓存穿透,没有的数据会短暂缓存一分钟,避免刷接口或大量不存在的数据请求带垮系统 + - 缓存击穿,一个进程只会刷新一次同一个数据,避免热点数据被大量同时加载 + - 缓存雪崩,对缓存过期时间自动做了jitter,5%的标准变差,使得一周的过期时间分布在16小时内,有效防止了雪崩 + - 我们线上数据库都有缓存,否则无法支撑海量并发 + - 自动缓存管理已经内置于go-zero,并可以通过goctl自动生成代码 +- 能否讲解下, 中间件,拦截器的设计思想 + + - 洋葱模型 + - 本中间件处理,比如限流,熔断等,然后决定是否调用next + - next调用 + - 对next调用返回结果做处理 +- 微服务的事务处理怎么实现好,gozero分布式事务设计和实现,有什么好中间件推荐 + - 2PC,两阶段提交 + - TCC,Try-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 + - 压测可以通过现有业务日志样本,来按照预估等比放大 + - 压测一定要压到系统扛不住,看第一个瓶颈在哪里,改完再压,循环 +- 说一下代码的抽象经验和心得 + - Don’t repeat yourself + - 你未必需要它,之前经常有业务开发人员问我可不可以增加这个功能或那个功能,我一般都会仔细询问深层次目的,很多时候会发现其实这个功能是多余的,不需要才是最佳实践 + - Martin Fowler提出出现三次再抽象的原则,有时有些同事会找我往框架里增加一个功能,我思考后经常会回答这个你先在业务层写,其它地方也有需要了你再告诉我,三次出现我会考虑集成到框架里 + - 一个文件应该尽量只做一件事,每个文件尽可能控制在200行以内,一个函数尽可能控制在50行以内,这样不需要滚动就可以看到整个函数 + - 需要抽象和提炼的能力,多去思考,经常回头思考之前的架构或实现 +- 你会就go-zero 框架从设计到实践出书吗?框架以后的发展规划是什么? + - 暂无出书计划,做好框架是最重要的 + - 继续着眼于工程效率 + - 提升服务治理能力 + - 帮助业务开发尽可能快速落地 diff --git a/go-zero.dev/cn/periodlimit.md b/go-zero.dev/cn/periodlimit.md new file mode 100644 index 00000000..74100aa2 --- /dev/null +++ b/go-zero.dev/cn/periodlimit.md @@ -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/zeromicro/go-zero/blob/master/core/limit/periodlimit.go) +- [分布式服务限流实战,已经为你排好坑了](https://www.infoq.cn/article/Qg2tX8fyw5Vt-f3HH673) +- [tokenlimit](tokenlimit.md) + + + + + diff --git a/go-zero.dev/cn/plugin-center.md b/go-zero.dev/cn/plugin-center.md new file mode 100644 index 00000000..13b5ff9a --- /dev/null +++ b/go-zero.dev/cn/plugin-center.md @@ -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) \ No newline at end of file diff --git a/go-zero.dev/cn/prepare-other.md b/go-zero.dev/cn/prepare-other.md new file mode 100644 index 00000000..acebdc4e --- /dev/null +++ b/go-zero.dev/cn/prepare-other.md @@ -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/) \ No newline at end of file diff --git a/go-zero.dev/cn/prepare.md b/go-zero.dev/cn/prepare.md new file mode 100644 index 00000000..146d23dd --- /dev/null +++ b/go-zero.dev/cn/prepare.md @@ -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) \ No newline at end of file diff --git a/go-zero.dev/cn/project-dev.md b/go-zero.dev/cn/project-dev.md new file mode 100644 index 00000000..67276dd1 --- /dev/null +++ b/go-zero.dev/cn/project-dev.md @@ -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) \ No newline at end of file diff --git a/go-zero.dev/cn/protoc-install.md b/go-zero.dev/cn/protoc-install.md new file mode 100644 index 00000000..5afd051b --- /dev/null +++ b/go-zero.dev/cn/protoc-install.md @@ -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) diff --git a/go-zero.dev/cn/quick-start.md b/go-zero.dev/cn/quick-start.md new file mode 100644 index 00000000..b7baa4fb --- /dev/null +++ b/go-zero.dev/cn/quick-start.md @@ -0,0 +1,6 @@ +# 快速开发 + +本节主要通过对 api/rpc 等服务快速开始来让大家对使用 go-zero 开发的工程有一个宏观概念,更加详细的介绍我们将在后续一一展开。如果您已经参考 [准备工作](prepare.md) 做好环境及工具的准备,请跟随以下小节开始体验: + +* [单体服务](monolithic-service.md) +* [微服务](micro-service.md) diff --git a/go-zero.dev/cn/redis-cache.md b/go-zero.dev/cn/redis-cache.md new file mode 100644 index 00000000..19961508 --- /dev/null +++ b/go-zero.dev/cn/redis-cache.md @@ -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七列数据: + +* 只查询部分列数据的请求,比如请求其中的ABC,CDE或者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) \ No newline at end of file diff --git a/go-zero.dev/cn/redis-lock.md b/go-zero.dev/cn/redis-lock.md new file mode 100644 index 00000000..31890078 --- /dev/null +++ b/go-zero.dev/cn/redis-lock.md @@ -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` diff --git a/go-zero.dev/cn/resource/3aefec98-56eb-45a6-a4b2-9adbdf4d63c0.png b/go-zero.dev/cn/resource/3aefec98-56eb-45a6-a4b2-9adbdf4d63c0.png new file mode 100644 index 00000000..603f8a97 Binary files /dev/null and b/go-zero.dev/cn/resource/3aefec98-56eb-45a6-a4b2-9adbdf4d63c0.png differ diff --git a/go-zero.dev/cn/resource/3bbddc1ebb79455da91dfcf3da6bc72f_tplv-k3u1fbpfcp-zoom-1.image.png b/go-zero.dev/cn/resource/3bbddc1ebb79455da91dfcf3da6bc72f_tplv-k3u1fbpfcp-zoom-1.image.png new file mode 100644 index 00000000..f530f572 Binary files /dev/null and b/go-zero.dev/cn/resource/3bbddc1ebb79455da91dfcf3da6bc72f_tplv-k3u1fbpfcp-zoom-1.image.png differ diff --git a/go-zero.dev/cn/resource/76108cc071154e2faa66eada81857fb0_tplv-k3u1fbpfcp-zoom-1.image.png b/go-zero.dev/cn/resource/76108cc071154e2faa66eada81857fb0_tplv-k3u1fbpfcp-zoom-1.image.png new file mode 100644 index 00000000..42bdbd1c Binary files /dev/null and b/go-zero.dev/cn/resource/76108cc071154e2faa66eada81857fb0_tplv-k3u1fbpfcp-zoom-1.image.png differ diff --git a/go-zero.dev/cn/resource/7715f4b6-8739-41ac-8c8c-04d187172e9d.png b/go-zero.dev/cn/resource/7715f4b6-8739-41ac-8c8c-04d187172e9d.png new file mode 100644 index 00000000..4513c5ac Binary files /dev/null and b/go-zero.dev/cn/resource/7715f4b6-8739-41ac-8c8c-04d187172e9d.png differ diff --git a/go-zero.dev/cn/resource/7e0fd2b8-d4c1-4130-a216-a7d3d4301116.png b/go-zero.dev/cn/resource/7e0fd2b8-d4c1-4130-a216-a7d3d4301116.png new file mode 100644 index 00000000..37e6fe93 Binary files /dev/null and b/go-zero.dev/cn/resource/7e0fd2b8-d4c1-4130-a216-a7d3d4301116.png differ diff --git a/go-zero.dev/cn/resource/Thumbs.db b/go-zero.dev/cn/resource/Thumbs.db new file mode 100644 index 00000000..9bd677dc Binary files /dev/null and b/go-zero.dev/cn/resource/Thumbs.db differ diff --git a/go-zero.dev/cn/resource/alert.png b/go-zero.dev/cn/resource/alert.png new file mode 100644 index 00000000..f24580d3 Binary files /dev/null and b/go-zero.dev/cn/resource/alert.png differ diff --git a/go-zero.dev/cn/resource/api-compare.png b/go-zero.dev/cn/resource/api-compare.png new file mode 100644 index 00000000..eb083a0a Binary files /dev/null and b/go-zero.dev/cn/resource/api-compare.png differ diff --git a/go-zero.dev/cn/resource/api-new.png b/go-zero.dev/cn/resource/api-new.png new file mode 100644 index 00000000..469a8cd0 Binary files /dev/null and b/go-zero.dev/cn/resource/api-new.png differ diff --git a/go-zero.dev/cn/resource/architechture.svg b/go-zero.dev/cn/resource/architechture.svg new file mode 100644 index 00000000..161e1262 --- /dev/null +++ b/go-zero.dev/cn/resource/architechture.svg @@ -0,0 +1,16 @@ + + + eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO19WXNcIlmy5nv/irS6ZvdpiD770m+IVSBcdTAwMTCLXHUwMDEwXHUwMDEy02NcdTAwMThLsO87jM1/XHUwMDFmP8qsVISIYFx1MDAxMaBcZrVduqsqU3FcdTAwMDSHXGL3z5fj/vn//cePXHUwMDFmfy22XHUwMDEz+69//fjL3jRqg25zVlv/9b/Mz1f2bN5cdTAwMWSP4Fx1MDAxMnn7+3y8nDXeVnZcdTAwMTaLyfxf//zn+29YjfHw52/ZXHUwMDAze2iPXHUwMDE2c1j3v+HvP37837d/Oz6nNpuNf37E24/fP1x1MDAwNiOiP/44O1x1MDAxZb19JlxcI1RzLMXvXHUwMDE13XlcdTAwMTQ+bGE34XKrNpjb71fMj/7KVUQ0XHUwMDFkrSZ4tbqj6ddwPNpZZt8/t9VcdTAwMWRcZoqL7eBtT/MxfI/3a/PFbNy3y93movP313f83O+3ZuNluzOy53PX74wntUZ3sX37XHUwMDBl6PdPa6P223u8/2RcdTAwMDN/05xbSlKJkSaIcEF/XzW/XHUwMDFmwohbXHUwMDAyIVwiseJcXCHn7fq5s8h4MJ6Znf1cdTAwMTd6e73vrV5r9NuwwVHzfU3r7fW+Zv3r+2JFLKqUYIJphjXB79vo2N12Z2F2aiF4XHUwMDFhWiFOmNCMvz+Wuf32RFxiRkxJ/H5cdTAwMWbMp0/um2+C8X8+3tFObTb5def+etulY+fmr7GfUuXx67XZ4q47anZHbbjyt6y9y+H9myi06lx1MDAxYpTIZ+nkcbF6mW5cdTAwMTNTXFx4XHUwMDE4/P5cYrO52sR8cea8+5xr7ljSXHUwMDFhN5bm00PI4oooxVxiIUJgpX4t+X+/N2WPmse3hLZcdTAwMWRcXFxc2OtSrVB9biSeWzTWXHUwMDFl72+JXGJcdTAwMGJ2JFx1MDAxOOHwyDnGWnhsXHRZhDOFXHUwMDA0UppcdTAwMGLOkZR7e5qMu06lNK/3P/14XHUwMDE3w7e//P7z//lfnqv9XHUwMDA1xLz2ROP97f7x4W3/XHUwMDFh1OaLyHg47C5Aj3Nmj7DF0XIwcD/hsIGMjl1rfrxcbrfaee1cdTAwMTe2/MPx3c+BXHUwMDFmiv3gR2J45qB66mT0ybZcdTAwMTOd0GqzXHUwMDBlt1fzYb37zFx1MDAxZUqvoWCjXHUwMDBmxlRYiIJgI05cdMXaXHI/gE2actBrhSShlIlcdTAwMGZcdTAwMWK7XHUwMDEy+EhsgeTAXHUwMDA3KKKVkI5b/lx1MDAxYnyQpVx1MDAwNcg5XHUwMDE2kmIsXHUwMDE5l/Ij+mCuXHTslIv33/6T+HOqsiv4YvBtNFx1MDAxM1IosHg++IOUIFxicVA/RSVcdTAwMTH4b1xyO1x1MDAxM4JOREVCLa3w27MgjFx1MDAxYkXwhiCGjZFcdTAwMDZAQFx1MDAxYVx0qm9cdTAwMGJBIX85efv9PVx0+S4gxFx1MDAxY1x1MDAxZc5cdTAwMDdcdTAwMTBcdTAwMTKEU8pcdTAwMTU7XHUwMDFkhMbIfrgvb+eNymM/223k0rJeXHUwMDBmuFx1MDAwYlx1MDAxNFLaXHUwMDAyXHUwMDA00lx1MDAxY2GmXHUwMDA0JuJdt998IM0tJLFcdTAwMDbhx4RJwKmboFx1MDAxMKiXpTTgIXhcdTAwMDJcYmT//VNcdTAwMWMgxKlcdTAwMDBF1IiC+DHQiz1cdTAwMTBcdTAwMDJ7jEBcdTAwMDRcdTAwMDPiXHUwMDA0abv3vNDzeqS8WoTqXHUwMDFi+1x1MDAxZVx0drev7lx1MDAxNL65olxcI4JcdTAwMThlzM9cdTAwMDeilEr4v1x1MDAwNK9cdTAwMDQhyiX/XHUwMDFjXHUwMDA2dVwir6uHfr1eXHUwMDFhtKd3NNSLLcIqu78pXHUwMDBlN1x1MDAxYu4w0kwxkFx1MDAwYlwiPTZcdTAwMDV7QpSCwHDM4LHB6lx1MDAxYrtBvkJiXqF9+fguXHUwMDEwxJEvXHUwMDA0cfAvXHUwMDAxhsi7zlx1MDAxZYOgZSw8z8Rj62ey7WeTu4dlJT99XHI2XHUwMDA0KWFcdTAwMTHwuVx1MDAwMYJcdTAwMTgosCO0Mb+uLC5cdTAwMDGEMVx1MDAxOEKw0c5bdV0vSFpcdTAwMWMr45IqXHJwRzxCMGpcdTAwMTlhp4IzXHRcdTAwMWW4XHUwMDA2r2FcdTAwMWaAmMaAlO9Bw1x1MDAxZlx1MDAwNaBcdTAwMTN1XHUwMDFkXHUwMDAwSFxuQmHvXHUwMDFjXHUwMDFihcdcdTAwMWW6XHUwMDBl2oWN7adcdTAwMDRshOJcdTAwMTCyfVx1MDAxNoBOREVcdTAwMDJRXHJ4Wlx1MDAxNLxKglx1MDAxOUNeqGj2XHUwMDA04KMpOJ5cbiw1u7lcdTAwMGLkJyTmtS9cdTAwMWXBwp+Z3Vj81D9cdTAwMGZcZlx1MDAxMlx1MDAwZWv6XHUwMDAxgoimXHUwMDFh/KAzIOjwI3ZDUGM2ns9Dndqi0fnzQFx1MDAxNGImI4HgXHUwMDExgoVcdTAwMDWXSIrfYvfTXHUwMDE5XHUwMDAyJLaoRFhcdFx1MDAxM4dcdTAwMGKGL0MjXHUwMDFi4nmFPLwhzaz3T/5cckBM7iNcdTAwMGW4bVxcS0a9XZ53nVmP70k7XHUwMDE5f6qmx1Fa4C9RfV/o/bUnl59cdTAwMDKm9085bHx+XHUwMDFjc5BhQW+RL/BoXCJqN/S6qlx1MDAwN9XJjudd77CI6k4/q1ZolY0/i+FgUb9L9JxcdTAwMGJcbuv7XHUwMDE5zZTK2Vx1MDAwZVlcdTAwMTft2GyiUX7nXFzQ5eipxFc8LNuz1ON2wdexXFzm9604qENcdTAwMGJ7s/BUXHUwMDFmwvzUR2u4KKliJ6tcdTAwMGbEfmIgQ6+t+/64PJSZ8l1k3lxmtlx1MDAwNVx1MDAwZoH5Pqg4XHUwMDEwZHyF3lCPuFx1MDAwMTuztr9ypUhzjaVcdTAwMTboZlozN3850Zy3xqNFsbszt5VcbtdP47Vhd2Bu4Ls78iaC8Fx1MDAwMWW7/u9ReDJcdTAwMTl0XHUwMDFitVx1MDAwNcjbv0fx7sxe11x1MDAwNlx1MDAwM+e9m9uD7ugtn0+w6y3Cg2579Fx1MDAwNsCwI3vmXHUwMDEy6lx1MDAwNbzl4PeCxXhy2LDYg0F3Mvc0K1x1MDAxNPlcdTAwMWUwgGdrgkpxul7EW71cdTAwMTDK3d9ccnfJOetvxLg+57GA61x1MDAwNYR1lna+Plx1MDAwNNdY+Fxcvu75gkdOz/nDX2pBXHUwMDE5+HWcXHUwMDEz71x1MDAxY967VsRcdTAwMTkr8KRdS98tXiq5lL3u8lx1MDAxOb+SVrx/SiazK4UqnIXii1x1MDAxOClccqL9XGJHi1x1MDAxYuM44b44zsDl1UI4XHUwMDEw5pi8NuPT6GOp31x1MDAwZqHK/UuYL2OFTbJcdTAwMTN0eSXKYv7yqpjwuXxNeVx1MDAxNVx1MDAxZcFcdTAwMTejXHUwMDFmpZWDo821/qPSeiGGXHUwMDAzfHuCNeWupV+E1dT/MFgxXHTWm6jTM6HTTX9U2lx1MDAxNNr5tJrcv26KuU38MeiZUFxiIVx1MDAwZlx0f8DAXHUwMDFhQnFzPHRE+pNduzV5LYQ7ndHLaNN4JdHosnp1rL5cdTAwMWJ2npPDskDb+uAuXHUwMDFiXHUwMDFmTGeh+/aFUKylrzhcIvD+XHUwMDEwo/h0LO4/0S1f7ULZ1PghkajP21x1MDAwMkWmXHUwMDAxXHUwMDE3R1wiySHfIVBYrMFyYqGOYfEtpfFyfzpAWCxcdTAwMGVgsdBcdTAwMTJjfYbw28VMflx1MDAxMG5tsrF2r72hyylcdTAwMDQ1XHUwMDAxP1x1MDAxYVx1MDAwZik3XHUwMDE0qz0s9tONP4HFXHUwMDFjcVx1MDAwNFx1MDAxMSk7Jv7h52HlVc+fM/bUftLLVGlcdTAwMTGebK5cdTAwMGXG3lx0kIvAmFJfMOZCMaJcdTAwMDU/vU6Mj1x1MDAwN5VOKz1pxTv1XHUwMDExW/VaiXlhXHUwMDE2cHFcdTAwMDRxc4mjclx1MDAxZlJcdTAwMDBcdTAwMTiTL1x1MDAwMGPmkd7YXHUwMDA3Y4xcYjxcdTAwMTPkd1xm+jXieCFcdTAwMWHnXCJfXGbGvqdzQvlcdTAwMTYpXHUwMDExXHUwMDAxXHUwMDBlXHUwMDE4I+r0zLh3yFx1MDAxYWzRZ1x1MDAxYVuYYcJcdJGUKa0/pPYkVlx1MDAxNiFcdTAwMWNCMcxcYlx1MDAxNZjfRPQx51x1MDAxNpZcbj5HXHRFXHUwMDE5I175cfDfOUg/XHUwMDAzXHUwMDFiiaSp3dhTXHL4TcpcdTAwMTH821M1vvqA7nBG64er2lx1MDAwN76TXHUwMDAwj9+cLiFMhfNA/lcxJbI0VUiCe6CkKcfaP42/zlx1MDAwMZ3j+Fxyc6VcdTAwMDH9JVx1MDAxN1xcwlx1MDAxZN/bXHUwMDEzRVx1MDAxNmVcdTAwMWNLXHUwMDA0waVgVN+6TtJXSMxrXzze329cdTAwMGb0PnFA91x1MDAxMUOuWSMgXHUwMDFkevrRITRCzZg6Izj3XHUwMDBl14JcckOEaUtoXHUwMDBlos1cdTAwMDFEXGKS71/3XHUwMDE3XGZxi4Kogu2VRCt+I4/QXHUwMDE0mXBcdTAwMDFcIm+KwkFcdTAwMTM9UIhgi3GhsUBaapD5vWJJSTVSRPic2X01XGJcdTAwMWTO1Lj0XHUwMDFkUbj/XHUwMDAyU1OUKCk8g31cdTAwMTBiXHUwMDE2MkVcdTAwMTBcbnFYJpQ4XHUwMDAyQi6NuvJpvd+jMq/9h/R9sEBI3yQ1wYhcbqaIPt1cdTAwMWI/fFxcXHUwMDFhTCygXHUwMDE0TJ1cdTAwMDY0oODlgrazXHUwMDBmUEDA7IAzXCI5UYLJW3kkXHUwMDAwSFx1MDAwNFx1MDAxNFx1MDAwMlx1MDAxY1wiU1x1MDAwNke9YkVGpcUoVljDTijT+746w1x1MDAxNPapXHUwMDAyUjF0OFPgxlx1MDAwMtizqbyR2DxcYinxPlx1MDAxNlCLXHUwMDExMPrElCiA88I+2blxskNiKrUpgrvNMGKSS1OrQ/a2XHUwMDA1Plx0x1x1MDAwNHx3ZKrKpFb0tk5JyF9QzGtPRFx1MDAwMlx1MDAwN0RcdTAwMDfyVFx1MDAwN87LMLhfXHUwMDAyIU5Ph6LMa+shK1x1MDAxMK7Xas+hSaKuQopcdTAwMTZcdTAwMDJcdTAwMGVFXHUwMDFjXHUwMDFl34eAiEvqyk2pXHUwMDBmuzlcdTAwMGZ+XHUwMDE0arVse1x1MDAxZn649MpN0b3clCamLN9cdTAwMTmkeidcdTAwMDPaj2zXmFx1MDAwZWU1PX1cdTAwMWRss+Fa4SXBrpVcZjgoX35JJ0fv1b5wmYBcdTAwMDM8kNN93sf+oNmayGyOqXxyWFxuzZ66637Ahcv51H6KXHUwMDE2cef8byNap+X8MTdOLNHIO5b+XCLJen+An0kzRaJZ5y36gjxTs1tcdTAwMWKOnWbb2YqC/FNNXHUwMDE4LC1cdTAwMDSBZ1xi/CxR2+ldP1x1MDAxYr/f6fB82HihvFZcdTAwMGa2wFOMsUWZgi9cboJFXHUwMDE5cjfEmVx1MDAxMJBA/GFqlVx1MDAwNVx1MDAwNIL4XCLp9/PrnJWuv8Wf4L2GWy6FaTfA3p7bu/jzdqJVx7VhPrZ+KKXUZD24u0tcXD3pz1uv5UhJJsLbWm2ZfI735st63OkkLfOFxiBcdTAwMTmuoFSulY3rVam1pEXngs620Fx1MDAxOO1cdTAwMWU6eN2NvFx1MDAxNO5cdTAwMTRb7SpR51x1MDAwMp1ehzPbhZxkXHUwMDFm6cCeZ8KdNY04XHUwMDE33Fx1MDAwZvK76qrCc8Viqsln05f068LV33ZcdTAwMWZNrCqJxyiZdObxULi9KNrSdi6IpcIvKZ18LT6GJ5Mxybwmm+GN61s0qslyWuR1Kk0jjWahmI09XHUwMDBlnVx1MDAwYlQnftdoytRuycMqwnG9PXp2RbP5cMjeKdVtz6fbqp1cdTAwMGatey1du+x0XHUwMDA0XFx46qe4WGDTT6zOUFxcXHUwMDFhm1VY4+4+VKrHJ4/D12qz8LRcYrriKmUpX8VcdTAwMDWv15JcdTAwMGXFvU085jgzPWC2IESWiPxZtb3QakXt1Y8nuzb8YtN1sIFcdTAwMDBcdTAwMTPse0QoyM+M9OkqkI6mysNaJlx1MDAxY3voduO9gYpOXHUwMDFiLOAqXHUwMDAwISV2uf1cdTAwMWZbXHUwMDA31DVPrGuk3mjhfSVQmvp8ym+VcHZ4/9JcdGX8aVO1fUQnhsNEuj8vXHUwMDBmmtv2y7xWfO50Vjh9dVO2qaVjVFRcdTAwMDdF8jSfglx1MDAxYcZe++7m7UpqlERcdTAwMGJRiUSTXHUwMDA1SauF6lNq6VpQKvO78aZHc612UUlcdTAwMTFrjJv00WUlcvHpmG3T60mxUHhpNlQzLMLOXHUwMDA1jajejMkwmmo2XkdcdTAwMDX01MvdtcquXHUwMDA1XHUwMDA11E2XNuv7ev41k6HVUdkuz5xcdTAwMGJESE1ym/A6kVtcdTAwMGVTk5fVulWOXHJOszOHXHUwMDE1XHJefoomwVx1MDAxZNGCnFFTXaqhxa6e3yV3y3ZcItqtbcWTrFx1MDAwNFxc0fDHfFx1MDAxZkXXLMy7rmphKiFKpVhcdTAwMWY7jFx1MDAwZrRuXS62ztOOjy5cdTAwMTJRhmTnnNjmML9DQMVWMJ+00E8hpn6dXHUwMDAyXHUwMDAxXHUwMDEwYk2kRlxcqmOhfqCF2LHgMFXVj2NsMrBcdTAwMDBcdTAwMTW6XXsyfVx1MDAxYcZmL53MpFWMlVnCuSCc3FTW/XlcIorvandTXq/j6qDpXFxQK61cdTAwMDbLVS+cQm2b3z+haCtSdS0oJyvieVx1MDAxZLtvTJelxuxxx0ey5+p0SyXrq00hvtCzot7hzCqyXi+Ic1x1MDAwMV2ti5VcdTAwMDXh43A+V5o0mpNEqEGdXHUwMDBi2l3aQ+mEYstYvstcdO5cdTAwMTWbxJVar5ZcdTAwMWHrJO2vas+b8rCu8pknXHUwMDEyXV1YS8b9kcA0+mqNzqiooVx1MDAxNbs6yIZ76Vx1MDAxN/J0n8BrUomna1x1MDAwMUdcdTAwMDJnh81bXkNJi6P312V5XHKN68grq0ew12H1XnwkXHTl7n7Fr1f1XHUwMDBi46P0sm7PRiA087NcIqSB3XJcdTAwMGLs9ep5NfJtr8ZMI9Nf4XBcdTAwMTCOyXxSXHUwMDE2m6m7QSfZj61T6dhDcjwqPfjI/I36q/HnJJ9Qd1QkuIWcgn9xRs8jLaA9iiadP/xb7oWiUuqjflpYNYr2w53qd0KdQbz+tC5kxl0nZHZbjXE53ExURXLTXG7zaXv7tNTOXHUwMDA1X2MkcyNRa9/nWo18rLxcdTAwMTjNXG7PhcjEZaDsdv+Jzzjtc4Gf51x1MDAxMTqLjWJTl1x1MDAxNX2dXHUwMDE2XHUwMDEy0YdIsZTb8cR96kHmOsxlXHUwMDAza5mBRJGX9eYlXHUwMDFhXG7Hy5XWY8yVc1xmZ+XLiN2/rDPTXCJcdTAwMWSxca3OUNK5oFhJXHUwMDE07JF6XHUwMDFhp+x1r5y5i91Ppugy86KIb3jEtYSrhJxcdTAwMWVcdTAwMWWNXjvRwd02dMdXvVx1MDAwZU9cdTAwMTXqz4859S00jTqsyNuvUHZFXHUwMDFic0GrXGLRmjPDnvit9ezSxr/c/XWTd8Nus1x0YvBZ+4TRXHUwMDAxXHUwMDAzZSoliKst+JjarDas+zBNNKdykY+FWqpRy1x1MDAwZfzSd8FSXHUwMDFiwd1qI+hcdTAwMDdcdTAwMDPFrq83p1ooTFx1MDAxNVx1MDAxMkzyo6xcdTAwMDW7XFxoXVx1MDAxM7Fpt1wi16tWhfae7kLxwKjOl1ogPKfwXHUwMDE1XHUwMDFhvW3nha9cdTAwMWHNeao/und9RLtde1xmr56eu1x1MDAwZolcdTAwMWVcdTAwMTkl5aKbeKg5XHUwMDE3vK7iXHUwMDFinkLTtF2flu95e1x1MDAxMFx1MDAxYlx1MDAxNCfOXHUwMDA1T9thYjSNZdiuuiyXVt1x+4WVLrRh2teGYUyEIFieUVazq4dr3U5fkucoXHTniyzRy65cdTAwMTPfQlx1MDAxYiVx1z9gSoJhxDCXjFPMxLEoKdiqeKFcdTAwMTUrXFy7S+dcdTAwMTQrdoDJU/nWXHUwMDBiXHUwMDExhTVgOzrd8ztcZj1BVlx1MDAxYUbcvZKYyJtcdTAwMDdZxKtmXHUwMDAyoz21UUgqwzV3LMhcbohWnFg1ezhcdTAwMWX/4SpQZfBcdTAwMDA4h4egXHUwMDE4XHUwMDE4clx1MDAwN4Hcr+pUaUmKXHUwMDE41opqjDUnv66fWTJ72Fx1MDAwMfvh6uFRxFSyUCEwYqZqf29LzGJUI1xuj82QJslcdTAwMWKz7DlEybyw41x1MDAxN/ZcdTAwMWVvoFg8XHUwMDE59ycs0NQ0QakzmmRcdTAwMGZ7NkHGn48uNNZcdTAwMWaSPDeAXHUwMDFmRzXHe8mWXHUwMDE3+kilj7N+fS/0OVnXXHKJsGKEXHUwMDExqVx1MDAxNEThlO1purAgykOMaUZccssl/2RcdTAwMDfhyXhcYlx1MDAxYlx1MDAwMluglMDwadrM1/DoKFwilsJYXG6GhDY8yfTGPYQhhzD9/HuwXHUwMDAw6FBcdTAwMTSPsX9cdTAwMWSamVx1MDAxZiE0kaenmcfliN1ev5R2tYd+L5foqqQmL99cdTAwMDKCXHUwMDE4c9efXHUwMDFkoFL5+iheKzBcdTAwMDQnJJqnoVx1MDAwMdtu1cM0ukwwO7otLlx1MDAxOEpcdTAwMDUmdHCofmDzyI5cdTAwMDWfzFx1MDAwM/x9r1x1MDAwZWqkb1koYr5cdTAwMWVcdTAwMDGYIVx1MDAwNa4gO/2gM52JzneZVnrJ6Gqk2K5MOzz8PbRR77XIXHUwMDEwRK9Ll+FR6cD21XE/kDfNwlx1MDAwNIzdscq3YCvjhXF87ClcdTAwMTJ13uE/XHUwMDFkyPNcdTAwMDNcdTAwMTVcdTAwMDJYMmqmnZxuxlx1MDAwZYNHoFx1MDAxNYcqXHUwMDBiLFx1MDAxN1bSzIHB4lx1MDAwM+dcZmbYeiv3Q1xu7olkXHUwMDBl4389q4aUpVx1MDAxNYR9XFxLXHUwMDEzuztI2ZyESJZJrzCJYa+aeJg8wVx1MDAwNGL8KFldQJToRMf7sH/kcnPhIVwigbjmXGJcdNOxvO/lXCLLlFx1MDAwMZiZPoZGhexP87mu51xysVx1MDAwMMT7gmhcdTAwMDZcdTAwMDG9oczw2JKyXHUwMDEwPHdGONJcdTAwMWHcR49cXMSV+2R9Zc289qQsWG75XHUwMDAxNJOHurqI5mc55Yc9nSCjXHUwMDE5XHUwMDA1N1x1MDAwMEFcdTAwMTRcIikynDDcPWeIXHUwMDFidlx1MDAwZlx1MDAwNlx1MDAwZpZcdIZcdFGXN+17uFx1MDAwNNhCXGJglIPB18yr9lx1MDAxMXNmmVx1MDAxMFOCuCNq6J32sFxmlJNRSfgxj+F7YdlcdTAwMTkhO8aGXHUwMDAzxNDrSCwodc5cdTAwMDH7e3ZcdTAwMDezNITyXHUwMDE0bjVcdTAwMDF9XHUwMDE1+HNgdlx1MDAwNr7CRiQ2XHUwMDE0VVx1MDAxY0khpd7bkrBcdTAwMTg8cFx1MDAwNZdcdTAwMDA6uML7VFx1MDAwNFfFMj9JM6/QvpB9XHUwMDFiKGO+VVx1MDAwMlx1MDAwNFx0ZXI06PSI5nBMXHUwMDE2ZChcdTAwMTNcdTAwMTC+UI1cYohcdTAwMTJiguBcdTAwMGb9XGKamiQ+PH9EuDFYN4hupLZcZlpcblxy4s6lRJ5YRsFgaqTM+Fx1MDAxNka1VyqCg1x1MDAxYfPjJMLfXHUwMDBiy07GXHIz1EhTKt5cdTAwMWXWzymb+26QtohcdTAwMDBjZbiWuXG1P+mZnZOlhb1QeC7gXHQxcL1cdTAwMTDdT4pcdTAwMTJiYU6YOZfBPzlcYm+LZ77iZl77gvZt8Ez6XHUwMDA3moJzSaU6g0npcClJkPFMXHUwMDFhTlx1MDAxY62EgsiaXHUwMDE598uFZ1xmgfFcdTAwMTTgk3FcdTAwMGVxXHUwMDAyxFx1MDAwYjdwzTSxXHUwMDA0MeONwfOnRFwir1xcqpCWIbdScNmQmnhcZqElXGJ2yTT9z/LNzjjMZVxmvr7xvaTQXHUwMDEwcjpPMt6Hy1xuRbTptKBcdTAwMTDesU/C2TlcdTAwMThrhlZcdTAwMWIuIFx1MDAxM29ij/NlbNxFQ1x1MDAxMGkwREOYfOtA01fY3q7uy1mw4OzgXHUwMDAxXHUwMDEw9T9cdTAwMDBcIsTQVJMzXHUwMDFjtHRcdTAwMTGlZ502203sUun+OTyy7ZLfKMlgXHUwMDAxmsJ7XHUwMDA3QCowXHUwMDA3QEwhsJJH2ZmX28KgIzarLVm+lpvJXHUwMDE5jldw36li/3P+83vBrc5/XHUwMDFjXHUwMDBi/kRcdTAwMWSoOVx1MDAxNvfXZ6WYJOqM3FEuNWyLZrpf1fh+OVwiT8nCa/R71JSoj2yvXHUwMDA06VtcdTAwMWYgnVZcdFxuPlwiXHUwMDA1p+Po5Itga/OFXHUwMDA3SKnxlWdnXFx0fiSl77Er44RrpM8onj6s9kHWXHUwMDE5XHUwMDA3JedPN/5jr91cclpcdTAwMTmkl1x0pPsmkGhBKWHkWPl0QJTiXHUwMDE2lVhMY1x1MDAwMVx1MDAxZTg23VCKeYT44M2bZKXhTlx1MDAxN1x1MDAxMHp90lE/7MO5glx1MDAwN0yYRGaws9BmlKfep3M/vqfrZlx1MDAxZD445vS7JFx1MDAxNqT/kY+AXHUwMDFiS5DzKPRcdTAwMThcdTAwMDJ5+1x1MDAxNd9cdTAwMDGB1N6oXGI3xYW6RWLUo+xcdTAwMDOzPVx1MDAwNOKmXGKdOZ2n/1x1MDAwNFx1MDAwMDpZ2d9cbtG1olx1MDAwMmvEOFx1MDAxMpLsn+JgYSmJTXGBXHUwMDE0ZlauR+H3tWvRwThrXHUwMDBlz1x1MDAwNSmwT8yDUVx1MDAxYYI4M95BKVPiZzZ2W1x1MDAwMFxuSeZcXFx1MDAwZnJcdTAwMTQsXHUwMDAwOpBcblD+LFGYXHUwMDFhxlx1MDAxZK7OXHUwMDE4ZZp4TFx1MDAwZcdcdTAwMGbDUTdcdTAwMWaq9Fx1MDAxYZVdY9Tf6K/FoM9xbXAqLIZcdTAwMDUnZkiG+lBBwzlcdTAwMTg9qkxfg1x1MDAxOVxirS6rnyFMXHUwMDBiu+lcdTAwMTFHXHUwMDEwXHUwMDBmomZC90e5m5MkXG6CfSyUeFx1MDAxY1x1MDAxNnfV1ch+XHUwMDFjJ8piXHUwMDE5e+zKl3n26mH9Uf6aeKuWWWRmy071wZ5Ntjry3N25XCJmOery1ehxoqKVynqyKNj97bOLcTS0a5eScvM0XHUwMDBi3Zc7K3s+rk86XHUwMDE3ksso4l9yXHR3VzFyhtS/XHUwMDBlXG4xsWqEnmVnWX+N1CaDSC/1XHUwMDFkpF5oZmmH1Lvz+cLUjd1cXOypV0+GJ4s0l+h4/HxLob+UjnPc6NuzXHUwMDFmXHUwMDA1u92Fj99eN1x1MDAxMv784DLMlW/yXGJcXFDEsFx1MDAxNKe39Fx1MDAxZoaDQPIsaUEsRZhQXG50XHUwMDAwJP1cdTAwMDP4XHUwMDFivjVGTZtcdTAwMWRcdTAwMDUhdJ4mXZOUVitcdTAwMGJJxTGWXG5Jqr2SSqCLXHUwMDE2M2Ur4NNIeDJ63zRAMEaFi+7+T05cdDnsXHK43UzAWy2Mm1x0qMMk0/tcdTAwMDfsRFqEcaqlXHUwMDA2t05BVPnJrqPDnIDuXSHTUiQ5NoNcdTAwMWPN8blH9SN3hSri1rGuv6CYV2hfRoLliVx1MDAxZeR+lMq3bMjcWsHZXHUwMDE5p+z5Z9LY1nr8ZUCLyVYn+tTK01xcsIFcYiwttrgkXHUwMDE4glx1MDAxN5A8rt2lj1x1MDAxOGNmcSawYVx0xVhfWMbd1DWh9pGIIc95Ifs92uA0UM5POJ96ZFx1MDAxNVbOb3S+cl9nzeog0cN1l75cdTAwMTXym/7DQ0f0krmcqsvibIa31yeDzORik9W8kGHpzFOmfKd3i1HqXG6EpcoxYXKveedtwOA5I29cdTAwMDfrViZUotNNLqzW69xWV+/XQVx1MDAxN1pKXHUwMDBmXHQtXHUwMDE41OBcYi1DTFJcdTAwMTevYqBltprapVhkXY3T6cNcdTAwMTNcdTAwMWWKu1x1MDAxN44215BZ/1x1MDAxNnSCiCT6LN6YVCVcXL2fxCpcdTAwMTk+ypLBsFx1MDAxNUq17lx1MDAwM85cck0oPiy0MkBCK8D2MXV8fk5AhLaQ1OhpcV9cdTAwMWKAOLyun3Kvj2PpXCJV95Zqx4JIp1x1MDAxZEpnX2r5tL0pJMeD5K6KXFys6z1VtOX4pVmN7VaDzNPrbFx1MDAxYu27OFx1MDAwYtfDeD1UScZkb5dJjMJN/VRvXTjaXFw5aEr2jtiR4Fx1MDAxOFF5us7MkixLVPVcdTAwMTUv4vepUKUzvrefXHUwMDA2QddcdTAwMTmmXHUwMDBleieEWujWOkOph854ZFx1MDAwYqRhR2H0KIXDXHUwMDFm1ZlcdTAwMGLzXHSJ7uKhVv9cdTAwMTG5/1FYjkbO7MCXJFx1MDAxNPxcdTAwMTRF+Fx1MDAxZqtjxSlF2slcdTAwMGZ+TFFcdTAwMDbhXHUwMDE1js90tylrLENcdTAwMTbt2MusulxyuKKww1x1MDAxZVx1MDAxMVH89orizF9cdTAwMWVQXHUwMDE0KlxiR+JoW3OAteBcdTAwMDHC1lx1MDAxZv/948meL1x1MDAwMqJcdTAwMDBcZms/XHUwMDA1YIIxycRcdTAwMTmDN5adXGLelVx1MDAwYlxu5DXcoo00ndJp0MNYLbgljXhcdTAwMGIpNNfuXHUwMDEySy6IJakpLFx1MDAwNlx1MDAxNWDObNs182mndfVjIVx1MDAwNJeCXHUwMDEzbzNxtXzZ52ZcdTAwMGWOm/ZcdTAwMTdL9JGBMv7pXHUwMDE5zEwm4Fx1MDAxYz7+nZzyyXQ+iY+fXG7l+4fQ83rgmOBcdTAwMTZIud4rXHUwMDE2XHUwMDE2XHUwMDE3llx1MDAxMzbsJmvW9oVXYFx1MDAwZuwmxGPcXHUwMDA2JYYvzJEs9uFIvmPzh2m51bJcdTAwMTP5ZK1XLFx1MDAxMjVcdTAwMWNe3fFXrUihue5VXHUwMDE5sofjNI2wx3pu6fSlsi/rtEw9LGP1yabQjqfvNqtW2rmgnptuX3TuKdQuXHUwMDE0hsnXhl3FO9e4jaOzMNaZRucus9pFO3amm1x1MDAxYm04joi2c8Gg+Vx1MDAxMp29jvlsXHUwMDFkb9yFcSVXL1ZdXHUwMDAzm+rV0VO0ukZzmq3lIZq17/lEOFx1MDAxN8yyPCFjjWm31F4slpPNSyFcdTAwMTR2jdto07hcdTAwMWE/pHE1Jsp5XHUwMDE2a47v7vqutLf3sFxmx4Lm8/28XFzOMDJR9+PxKv7S7eS615lcdOXvm1x1MDAxMdNRIdhcdTAwMTms57nJvJQtNcqbZatRXHUwMDE1o2xrNeuGgq3Dcm9cdTAwMDY8djM3f9zK12q00m/EauR/XHUwMDE0+u9cdTAwMDVfoNCr5mD8iGKbziZXyZPUXUxcdTAwMTCqXFxcdTAwMWaxK65ytdmm0l3k7V6yWirUO+JcdTAwMWHqiJTvWHShIVJiZ1x1MDAxNO33yv1R4m7aKJdVup5YKZaObkZcdTAwMDFXRrlH+CSJti5rXHUwMDFivNioXHUwMDEyzIX71v+PXHUwMDBlXqqDncdcdTAwMTZfle9iqzZcdTAwMTFRjOujzX3WNWAquZhVUlx1MDAxYcxcdTAwMGbO1pPlZVtcdTAwMTaneO5cXKBxNteYL0Y7vCmz7mMvvW6EQ2dp8a6SKU9cdTAwMWaf66NcdTAwMTeVwXjxsFDFpGtcblZT0H7Onlx1MDAwZVGkXHUwMDFhX9w/Z5GIZ+uXJVxytaOi9OPhkDRcdTAwMDNcdTAwMTPoXHUwMDE501x1MDAxMkSGPPbyLUmfwttenKV5b/XQXHS2hlx1MDAxYnLQXHUwMDBmPjO7jNfVT70x4vvq7Vx1MDAxNfExU8h6tHbulsp96dSqWqtfc96kP5jqwNK/71xm5FtcIinO8CeT+O6pv+Tx10Gxml1kK9txpuPXd1x1MDAxNlx1MDAxMFx1MDAwMZfIb9LAz2GNUlxct/3skonWZi5cYuaGT9xT+P+sVMe7XHUwMDAzu27XgpLDc/I2fSxFYVx1MDAxMp01elx1MDAxMFdSM4W342ZlMZznXG5cdTAwMTmMKrlVsMVcdTAwMWHQ1D1RI8SQ/FxuSdZcdTAwMWXzXHUwMDAxPCSZXHUwMDExrVx1MDAxMPdj6P6zkjxcdTAwMTi3Q5PuYFx1MDAxY1x1MDAxNFFm3LfCn1x1MDAxMWXKXHUwMDFlz4DoxYxHmlE7nbRpe/NYy4jXQkhcdTAwMDRblqnhv5SYSzO2gIPUuFx1MDAwNVtLQ47JpZZIYMM9cVx1MDAxM8Em+iT3hGDTdyqw+ERcdTAwMDHn7YeRXHJAJn/894+CPVx1MDAxOc++WrpcdTAwMGZcdTAwMDfSgvlcdTAwMWW5mDCaXHUwMDAznJ/uZydcdTAwMTb5QfNRl8vJZX0xrJNyp1x1MDAxNZ1cdTAwMDdbxjFcYrD1s0KVSc2p+iDkhlLFUMRcbiY008zp/35GyrVotmyPQ0fOqOUh55i5gvpfoo6ENOMujtZcdTAwMGa+ju1Qf9NYqG2lNJIqXHUwMDFhX2ZcbtdvY7lCnD2dpl9ao+om9op2u1x1MDAxZGt1bCmvXHUwMDFi42ayvSmZXHUwMDBmUGvajsRcItVcdTAwMDUv93PpaySihPItYjQ8s4qLc/ph0oWQimVS5S1+rLVmI01e2yRcdTAwMWF0XHUwMDA1YsxS6rdcdTAwMDJ9nLqOkVx1MDAwNFx1MDAxOSZBUiBDoqZcdTAwMTg62lx1MDAxYv9cdTAwMWajP4t4iG5ecZpNoupp2Fx1MDAxZWWGoy69hvQ7ulr3yyEx5Vx1MDAxMMeecWKfj6FpOXzHYpmIrSuLlOxOVyTo0k/0QenHXHUwMDEw5qJAST/YXHJcIlx1MDAxMVfoWIXXt1x1MDAxMf9jaVrRaYafXHUwMDFlSynV7C71XHUwMDEwRDQ+fVx1MDAxZThcdTAwMTf8iVx1MDAwNCcmyjfDqbmG6IKfUeyFU2U5XHUwMDBll1x1MDAxYTKdKbSGW90tbFZ+TK9BUVx1MDAxZEXQYc+LM0ve3PM6MVx1MDAwNaSoXHUwMDAytNM+9S5fozRcdTAwMTdGIe1xaL6ozTvO+/Sn41x1MDAwZlwi/NP8WEhwXHUwMDFl2OlK0KpvUFwin6WTx8XqZbpNTHHh4ctKgz/H4GF4jrVmpvBLSs3kx1x1MDAwMVx1MDAxNJpZb1x1MDAxN4Q01KHowk5ihVqtOtrXXHUwMDAxSs1cdTAwMDBLiPMpKCPiXHUwMDE0e2hcdTAwMDRcdTAwMTKgrEwrpMHcaWfv8d92XHUwMDA1XHUwMDE5YlMkj4Ylvdet6HcyoUSv0Gbx0bzYSoavb1eOXHUwMDFlveUqXCKajlZcdTAwMTO8Wt3R9Gs4XHUwMDFl7SxdZiPbTnRCq8063F7Nh/XuM3sovYau4DU5XHUwMDA3x+85TUqYgTnq9Jjbjj68RO5X41K321x1MDAxONS6xVxitef9YFx1MDAwYj3X2GJcdTAwMDI8IyFMje9Hcm+CsVx1MDAwNXdAS0rVXHUwMDFih2qAZZ5cdTAwMTmeXHUwMDE0LXzahr9a5sfIfrgvb+eNymM/223k0rJed4l0gJXCf7QwXGKBkNI5qfqYTmRmtVbx2ZDJTcuPs0kr3Y/dfVnp7yd1QoKz4zhcdTAwMGaTXHUwMDFmdIJcXOb93Fx1MDAxOPlcdFx1MDAwN1x1MDAwM3Z8xtd/uFx1MDAxNjhcdTAwMTYsY+F5Jlx1MDAxZVs/k20/m9w9LCv5qYsnqlx1MDAxYs+uQ9GRXHUwMDFki+lFM4ZcdTAwMWZcdTAwMWYqXHUwMDE56nqHrW6NO0lUbVxyM7Ft6LlcdTAwMTZcco3i3Fx1MDAxNbS8zpakLFx1MDAxZeqLRJtNXmov2dGzqzLkaOmId7nlRVGNM1v/MVx1MDAxZMZcdTAwMTElSHN8ejos9dpCzXy99Vx1MDAxMFx1MDAxZqxG8VCzkIzObFx1MDAxZj3u1Fx1MDAxYZ2l8+pcdTAwMWZz6fTPQ5NfXlx1MDAxYv6gyYJZhGJNXHI5ISjVhbPC/fTaWa76rsn7JVucXG4zXHUwMDEz64/aL8+whpkhXHJcdTAwMTD6USVcdTAwMTUlwpFXOVx1MDAxMuVk293RxnnT3kNcdTAwMWPtWvxcdTAwMTVccl3+XFxJZt4uePfntLjby2yN3j2J0N1cdTAwMDRUetyuJ7L53ddpXHUwMDAz+pQ2YIbAm0OGUUPA98XaXHUwMDFk4Vx1MDAxOHZOS1xiM35cdTAwMGYxyehl8Y1fiVx1MDAxM0cnMSVxbGj0j1x1MDAwZjfITEbLUqEw5LmdXG6FXHUwMDFlJ4Pua6ZzdTOm0+twZruQk+wjXHUwMDFk2PNMuLOmkf1P+VQq4G7ZXHUwMDFkNH/8949cdTAwMWPcxFrbhj9F7clg/NXMSlx1MDAwNz1C51SfjyeTXHUwMDEwKVMnpeJR3XlMbIRKhLu9PJtlY4PIcyVcdTAwMTf1Sy3fQHc+mSGjXHUwMDFjOXTHzTHGhXYpzkXl94tZbTSf1GbwRPe1R2CP0lx1MDAxMm4qkL2r//+uXHUwMDA2psIkmeWxivxcdTAwMTKWyX5q9/Isnpb1XGZJNUqrkstN+Vx1MDAxYXU72lt/lGyvX3lcIk/VReEx2V/1XHUwMDFi49xsvWy5Jrw1Uq1cdTAwMDdVW1cq3Vx1MDAxOX+qtlx1MDAxZWqTNXa5dEe78y/g8zuoa5r5ltMzQbRW9Fxmr20ra9O7Sa08XHUwMDE4PT1lXHUwMDEz9ex6Wsg9XHUwMDA1305hbkmHnXJcdTAwMWbkXGImXdp2m1JcXKakz1xmXHLHuIt9PiGmXHUwMDA0pfz4vPPWdtt5SKXtnUyNpoPY+LX/irdO8XpYr0RpPcuPq4tMa1todZLUXHUwMDFkUtxQV493hb5/ylGiXGZU6HbtyfRpXHUwMDE4m710MpNWMVZmLkVq0lQ1XHUwMDFhX9eXkf7DOj7qXHUwMDBlZpllybnAm3vzJE3z9Vx1MDAwNv1n+3GiISRwXHUwMDA20EdcdTAwMGZ8yHY5bWZlY9QqxaPRZHcmJiz4SkaJS8nczqDEysK3VzKH43eoUlJTZPg76bfWqptcdTAwMWYq3Y3XP0K/vMdcdTAwMWa57uSn03hVN/KUUVx1MDAxNUe6xfxcdTAwMGI5qVx1MDAxNkJrfFx1MDAwNlx1MDAwMU1qXHUwMDFk0anGsq3vspPXSqjSnCVXfr2bXHUwMDAx0jzQrXfNI8jdkK00cWneZY1ktpkmw65l3lx1MDAxNGzaTEQ85keyxDSXlqyUmTw12lx1MDAxOcV300ghXHUwMDE3XHUwMDE0PfzPt25S+ibwzZw8iTU+I16jJFJb4XSsn+DZRoKkXHUwMDFiw0ioXHUwMDE4eCVcdTAwMDNzccCHNGy5Tlx1MDAxZvKy6TB+SobZSTxPhlx1MDAxNVQx5qCz+4ZadfuxS/ao31x1MDAxZM2va88uS4so4lsxgbnSXHUwMDAw9PRcZlx1MDAwNsJO5HX10K/XS4P29I6GerFFWGV99CxQXHUwMDA07Fx1MDAxOEtcdTAwMTf7wMczZPFhXHUwMDEwykW61mpKXHUwMDFie+hcdTAwMWFRzKvsjsl9fmksuFx1MDAxMmbA6zFvMr6UKltar+usb6NE+TE+T95d3cRcdTAwMWM9njp6htZb5Fx1MDAwYjyaiNpccr2u6kF1suN51zssorrTz6pcdTAwMTVaZePPYjhY1O9cdTAwMTIuso+jJ2DeXHUwMDA3XFxcdTAwMTdcdTAwMTkpof2Vh1x1MDAxMs5cdD2HKHlWuE9vXHUwMDFhqedYtbqtkrhcdTAwMWS6izzXfZQnMEV32JW2o1x1MDAxZk6nNHf1ql3K2e6jN541d5joPa3hQlKClbid1tzchFx1MDAxNO3Zqtuw/z16XHUwMDE415r/XHUwMDFl3dVcdTAwMDY1XHUwMDEwOG/KQUyw6/e/ZIhcdTAwMDHyTbMrKlx1MDAxMfxzhjk5rNNcdTAwMDE2J1x1MDAxMFx1MDAxMbn04kPvvdKWXHUwMDE50SQ4+G/gYF3Yie/rulx1MDAxMeaOjzyo2KiFNFh5XCJcdTAwMTWnXHUwMDEyQ/S6P6NbXHUwMDE4enFBfLitvnpi1mFcdTAwMGbjh3uUXHUwMDAx54JcbsnMmbSm747sj98zXHUwMDAztMvoo89OMjhcXCDk2Fx1MDAxNLIoJ1x1MDAwNDGNKEOUS+acV/X3eFx1MDAwNVxyXHUwMDFid5yEkP1NXXeQga+YmFdoX0Le33BcdTAwMGZcdTAwMTFcdTAwMDM108+fJlhcdTAwMGKGXHUwMDE1PcexPVxcXHUwMDFkXHUwMDEzYCTiXFy5ZNyNRFhYXGJjXHUwMDA0XHUwMDEwpFx1MDAxNGJcdTAwMDKxy1xmtC9cdTAwMTJRjyM/8HUhhMdmhDxmVJL9Iz9NmFx1MDAxMTifoPKrh6icrORcdTAwMDZ5iJCmz9mwhVx1MDAxYojf13K48/KIlp9cdTAwMDQ9J+OhXHUwMDE5XHUwMDE3SjhcdTAwMDRcdTAwMGVaMlxi1LljXFzCO/RcYpfV8sLD647rc1xixtvf92RcIlhY499843/gaciFJCfnVOb0hdyk4/F4cXH/Sif5p9g0071cdTAwMGJ2IECYXHUwMDEyXHUwMDE2hDtCMcRcdTAwMTUj/IPHozG1XHUwMDE0vFxyIFxyXHUwMDAxOaQ3Yq2gXuHz/lx0p+kjxFx1MDAxOFx1MDAxZj3hbKyjs2WLjuL3hcJzqN6Yvuafb9t9QyzNwC1kplx1MDAwN1CD33JqWJBcdTAwMTmPuouxXHUwMDBm8zhxLf86Llx1MDAwMHVgoqVcdTAwMDBcdTAwMTExXHUwMDA1+ScrxWjHaM9cdTAwMGVcdPtx9Jhd4mQkXHUwMDFiKi2Crlx1MDAxNFx1MDAxY1tcZlx1MDAwMM3YWCHlXlx1MDAxOEDBNjOGYVx1MDAxOfjYlF+UVTpYc1x1MDAwM5+PLVNcdTAwMWOniZKcYq+criRGgZE5wFwiRjn267Jcck+Ado0h9VZcdTAwMWKSyzbjbV15XXTqVVx1MDAxYm5w5pW7XHUwMDBlXHUwMDE0bqlX75/SXFw9L5uDbCa53ejosJZ5SiW5a45tptNnu2e+jFRQvvXSnz82kt2Oc8HRZtL1YDRcdTAwMWVcdTAwMTbnlfhmw+bTx1Fr2Vx1MDAwYrumXHUwMDE2XHUwMDFj7TZd5lx1MDAwYo1BMlxcQalcXCtcdTAwMWLXq1JrSV1lQJ1toTHaPXTwulx1MDAxYnkp3Cm22lWizlx1MDAwNXE9TubLNJlcdTAwMWPmo+Hti93OxFtPzlx1MDAwNfeD/K66qvBcXLGYavLZ9CX9unD1q1x1MDAxZW34XHUwMDFlt59cdTAwMGKh5F33rlAsb7eheHi56O9cXFx1MDAxZlx1MDAxMU2sKonHKJl05vFQuL0o2tJ2LignK+J5XHUwMDFku29Ml6XG7HHHR7LnSt+lkvXVplx1MDAxMF/oWVHvcGZcdTAwMTVZr1x1MDAxN66eWW/KXHUwMDA1x4LR6+SlzPOJUj68jUR7vN1kYddcdTAwMWVUJ37XaMrUbsnDKsJxvT16djf2htQkt1x0r1x1MDAxM7nlMDV5Wa1b5ZjrRqUnjV1rkWtHRy+xwjxRXHUwMDFhRzI7XHUwMDE3X3AmgzftzD0jdracLW0r69Q65fpcdTAwMTberfOOXHUwMDA1+XDI3inVbc+n26qdXHUwMDBmrXstfeJR2uHOSHRgPFx1MDAxNiVcdTAwMTKfNVxccqlskt7p0oRPcDc9mqYq4UHZXHUwMDA3jFx1MDAwM1x1MDAxNVx1MDAwZVFwai0suMlpaHBcdTAwMDA+NEhKrcEtx4hiXHUwMDA03orSXHUwMDE3VUH+l92qgbPjkefH2lx1MDAxMuDeXG5TXHUwMDBmQpGUXHUwMDFlcEwlM43+jMOjIVxia+XRLIZcdTAwMTQ34fox3lx02lpcdTAwMGZsnEvo59Zo3blcdTAwMWJcdTAwMTJcdTAwMTmPuFxuXHRcdTAwMWbkbFx1MDAxZEfpx/RLs17rqVx1MDAxOVx1MDAwZWVrU+eCXHUwMDFiXHUwMDAy+lx0gdrnXFxyTXxcdTAwMTORmEpFwVc8o1xiMZNYt5I5sqnW5fZJzVx1MDAxMs/NRsnvYCsg3lx1MDAwN0XgfVBuMt5cdTAwMTDsUTD7bllcdTAwMTfgbMJlpeA6o+oy7+OAS04sppmZZSZcdTAwMTDm1CtcdTAwMDepzFxuzVx1MDAwMYBcdTAwMThcdTAwMTVKKVx1MDAwZo5cdFx1MDAwZW9hnJf/cFH3iVx0XGJRlJpcdTAwMTFcdTAwMTnaNK8w16pDc4lmtVZt5ENRinxv+pdcdTAwMDZcdJr6R85UIUH1OZQvtJ2UjbKtN8/jx8mzzE9cdTAwMWKZuFx1MDAxZnFpYOryXHQ3hYpcYp6FQlRQ+aGYKsQ1hNbStEoqqbC88LSg2WI28axjXHUwMDE0llx1MDAwMJNnXHUwMDEyVmAktUfrJlbIQlx1MDAxNMyVgiCBKan3XHUwMDBmXHUwMDBiXHUwMDEwQ0RDmH+M11wiVXyURCUzm9qMvC70loVUKfqNXHUwMDE09XM2SXF/m6SFXCJIO5k0jzahqGSHdJ9Ku6U90aFcXGazeURf1qr/SVE37F6MXHUwMDAzlCmQMsPC8kHUibQwMzlfo1x1MDAxNM6zxOuSm4JvZY6BzVx1MDAwMGtzTO9hk7Q5qVEm/1xm31xuVG+/XHUwMDA0g1DQXHUwMDA12Omx9q7vLuqeNolaXHUwMDAwXHUwMDA2xLCWXG5ixvudapJys/HQXnTspU9cdTAwMTFcdTAwMTT2ve1fSiODiG/qSlClkXR2R1x1MDAxY1x1MDAxZFxiMVx0P99cdTAwMGZWu/5cIlssrOTDk76XhW9glEBTmVJMXHTEXHUwMDA021x1MDAxYtdiSDc4p2ZwXHUwMDFluGf4supDppWsi89cdTAwMTkloS3EuUDmoIVcdTAwMTLikbZcdTAwMDJ/n55ANc8q2cYqa2dblFx1MDAxNW2ZLPZcdTAwMTSXkW+kqJ+Mk4SvTZKYI0yc826PiXotXHUwMDE2ra53qM3Zbt3IbaOdMGpcdTAwMDecOJ6AeFx1MDAwM5gpsExcdTAwMWFcdTAwMDJz/YE2zMxBgUtcblNGIIo6LOefM0eEXHUwMDEz61cmXHUwMDAw/HBEPYqZjpsjTVx1MDAxMfjL+Ohs4u8u5Fe1RjFzhthtXHUwMDE07dqs4cNjXHUwMDE2XHUwMDE0g+Q/KJxcImHCwtOTd73+ZrhI69YwgYp4XHUwMDE1i09cdTAwMWXuVzNcdTAwMWYlXHJW8o5IbDRFg2vIOf5IhVx1MDAwMapjKYKJZIbe59KCeNqsq1rdQ1uvk7zjXHUwMDA0bJV2XHUwMDE2KXrr61x1MDAxNJeLsteM9kv95Pyhn1xiPbLC/Fx1MDAxYunrJ42S8jdKcFPpObnqejxZ7ZR7/fv5xo5un/OZalstg22TKEi6xSlcIoxcYsJcdTAwMDXby9xcdFx1MDAwYqxcdTAwMTGELoojflx1MDAxOceFn5hj7NHWSPdLasHtXHUwMDEyXHUwMDA0qWN9XHUwMDFm312MPc3Op1x1MDAxM3Ppbt03L3ezkt3DeThOfVxyXGbgnFJcXJxRwLJLriesutncVfFzrr/Rd3k74mdhglx1MDAxM/EwxC3CpKBmXHUwMDFlOHEyhf4kz+Tceitxh1xikDJxoXlptWogN/t6x4VXhpzvXHUwMDE3x0mGmUnFXHUwMDFky7SNev1cdTAwMDHKlVx1MDAxONv2lnYuVtWp0p1rqNzDvFtcco9cdTAwMWH1cTqDXCLb0d24gFPNq7eIVFKjJFqISiSaLEhaLVSfUsuxc1x1MDAxYptaOkZFdVAkT/Mpbydir/22a4F+ude9cqM7f64+dFx1MDAxZuhcYndjz1XXXHUwMDE34ZNSP7WOkJdxZlpLLedT2og7XHUwMDE3XGZWeFRtbjOJelxyp4rd/mxiP7taq2dcdTAwMGZPhfmg/FBNXHUwMDBlWuP0czpTnqVcdTAwMWHOXHUwMDA1MlTJqFZcIvlcdTAwMTKbi0dt73Bn2nh2LiiV+d1406O5VruopIg1xk3qarZUufh0zLbp9aRYKLw0XHUwMDFiqlx1MDAxOVx1MDAxNmFcdTAwMTeMRfVmTIbRVLPxOiqgp17uruVcdTAwMWHh1yigbrq0Wd/X86+ZXGatjsp2eeZcXEBX62JlQfg4nM+VJo3mJFx1MDAxMWq4idq6tIfSXHTFlrF8l1x1MDAxM9wrNsmdc4FYl1P56utrSI6zgy5r3XXlzsUsovLF+Vx1MDAxM25cdTAwMTVwV4Zmk6TaJbJcdTAwMWR0IYE1xPp+XGLEXGYhXHUwMDA1PqfhszR/rPKcPdaZMMmR3q6weM77TT5cYlx1MDAwZVx1MDAwMlx08C3BpIBnSVxi6PxHXHUwMDA0YtSiSnOJXHUwMDE5XHUwMDE22iRfboJAnlx1MDAxZJ9cdTAwMWXdNFx1MDAxMLZcbjD+R3nfb1xiQDfvtinYze7836PMtph/gP+MR+1x9M7Tbt+w18Z3npQ+cHTGwJxRfYbGsN0yVu6p+2Tpsb5tPtebqE38KK1cdTAwMDJcdTAwMTVcdTAwMTXCXHUwMDE3RVx1MDAwN+w2wYZcdTAwMTJbXHUwMDBiQlx1MDAwNVx1MDAxNpyKy7hcYnzd5dPmpSlcdTAwMDXPXHUwMDA01PeYu3xLm3yhSkRri5qR+/O6mFx1MDAwN3ZrcVx1MDAwYlx1MDAxZNDSl1x1MDAwNEdAXHUwMDE0rsg5iZH5o3597WTz0d3yblx1MDAxOduIh8ayXHUwMDEw8Fx1MDAxMlNTymQpM4TP8KVLIdxcdTAwMWNcdTAwMDEhqblcdTAwMDXWxJSBmEZudVlBU6tlXHUwMDBi5UFcdTAwMGZ64lA18FkhZMXOql9v8Y8+1KSK4CUqXHUwMDE306XXaLGCtuFVMCyCXHUwMDEx/1x1MDAxZuXazO6Ml/MrM9NcXFx1MDAxOMI5WfM+VvhxZJhcIs6YL/j0XHUwMDEyzlRf7lx1MDAxM6VBqJMhrH6X0U9cdTAwMGZB11x1MDAwNTPeXHUwMDAw1ICZLiRnQ9HPrIlUXHUwMDE2u7kqcOThPVx1MDAxMY/4TZnCK+m44DP/+KG+yz3kQqrCOJuFXHUwMDE31V10mXR6T7fUXHUwMDE1h0FcbkBs9cmoxLHgk/W7f9/MS3TT/0CZcUE5SOXp6ZXY60Dxbunp+bWaWKdcdTAwMDdcdTAwMGZ3mW1VXHUwMDA2XTflId2UXGJ/gZniyCO34qGb8DhcYmyGXHUwMDFko4j63rp5XHKpdlQr7dUzIXiOmp9Rz9Tutrf3m3E1vqmsXCKz9i6z068o6GLNxCGxJvRLxNoj9vAyOZxJLJFPI/9/ilg7TU6WJ2SsMe2W2ovFcrJ5KYTCrqaTo30tzef7ebmcYWSi7sfjVfyl28l1XXvYXHUwMDE1V7nabFPpLvJ2L1ktXHUwMDE16lx1MDAxZHFhXCJcZlx1MDAxMV+tMoMxND6j9uieR+1cXL5cXO32oovmmKfvQ6NoLOg6JdAhN1x1MDAwZesv0CnmlYb3XHUwMDE4XHUwMDEzjVxmOZs+noX/ozp1YchcdTAwMTNcdTAwMTl0XHUwMDFi/bdw58db9FPczlx1MDAxN/bwi0NcdTAwMWZ/wlx1MDAxOeVP7I6Ng33OyLdlo5osp0Vep9I00mhcdTAwMTaK2djj0EddXHUwMDAylVx1MDAwNKOmppUhQyqjiWnncDc2MSosxN84wzBlyOFrXjNcdMbMlFxyoSiShlwiVUmPpFx1MDAwMKHYQlRrXHSbUFx1MDAxY/7jQfSuzVRcdTAwMTfsqVBfzfkwS9R2etfPxu93OjxcdTAwMWY2Xiivufo7XHUwMDFknFx1MDAwZlx1MDAxNO4v55hcdTAwMGJC4CEwuk+vgJWliFx1MDAxMlJcdTAwMWKqXHJcdTAwMDXw9je9wZmcXHUwMDBmzzFcdTAwMTZcdTAwMWKwZ1x1MDAxZX/todpo+zTIt1wi3GtXyFwiXHUwMDAwpsRcdTAwMWPRa2Wm5+K9PVx1MDAxMfPIJCVUUdg+6Fxu3dvTdSlcdTAwMWZ8hcS89sXj/f32sDBIZDOOs5g9tlLMkXZF+kdcdTAwMWRhz0FK31x1MDAwMIVcdTAwMTTHLrKZdyX+yaJ44cxV3+Q78cq5eMyQkFxcSVx1MDAwNlx1MDAxYVx1MDAxMVxmcDmHUFx1MDAwNmPKXHUwMDE5w1x1MDAxMvDDdOyJfUJcdTAwMTnyt+qdXHQmOznlk+l8XHUwMDEyXHUwMDFmP1x1MDAxNcr3XHUwMDBmoef1ILzxXHUwMDA2XHUwMDEz05khMcdaYS6xg53ifVx1MDAxM/i26OF40uZcdTAwMTWCh/xdXHUwMDAwglx1MDAxZFxilYmhKTsnXHUwMDAzdPiIP8BcYmHGz7jYxtyUXHUwMDE4jIOQSVwisaJagSNwXHUwMDFivKCIuVx1MDAwZVxmXHUwMDFkztL7gbdcdTAwMDVQXHUwMDAx9omB0YQnQ/fARGipqVx1MDAxMCxcdTAwMTjkVGjbwcWFvS7VXG7V50biuUVj7uqZdy1GZjpcdTAwMDSXyFBUMXOzhYdcdTAwMWG7YVx1MDAxY7NPYsvBOrBcdTAwMGZcYschssVcdTAwMDT+p1x1MDAwMOyY8lx1MDAwMJdcdTAwMGZcdTAwMWO8StxcdTAwMTZs/MXEvPZcdTAwMDTku1x1MDAwMFx1MDAxMfU9McUgzVx1MDAxNOTiXGbO2sOlREFcdTAwMDZcIqpcXE/XjUNcbsIlyiTWklCOblVKTt2FXHUwMDBi3KPiVlhcXGkqMTFz5CRlbK833vBcdTAwMThCyMeD4dOco/GwbVxy31x0XHUwMDE5inHCPUOTY/p+XHUwMDEyXG6dgY1cdTAwMTjiJVx00ZvCyDFs8X1H/CguXjdY8pWQt6v7wvFNMFxiU+k/11Uwjd683ZNB6PDBYJBBiJkmS67BrUZKaUw/0NFgISz4oelcdTAwMDBcdTAwMTDwlIm8LNnp29KCqEVcYlx1MDAwMyCRpvXai0SPMGFphaiJ2E35Id0v7sBIgmtB6Cd4gv8wXHUwMDEwXHUwMDExqeCLSSRcdMQ0oP37io+xeVx1MDAwZSbVKKVhYqCfdIhcdTAwMGVXVXyER1x1MDAwZVx1MDAwZVx1MDAxYVJcdTAwMWFcdTAwMTRCc4+IXHUwMDBmw1NTJipEXG62rpFHOum6aOQrJz+v7onIN0EjrX3P5k1cIlJgoU6vITtcXINcdTAwMTBkLIIvainMIDLA4OtcdTAwMDJcdTAwMDC7scjMbXeeu+Db5JBcdTAwMDFcdTAwMTKtt0NcdTAwMTWOmYJgwPPIXiqLg1x1MDAxMFxuXHUwMDEwfMZccsH3RzBcIlxiXHUwMDAySIOnwYjOzlF7YjiDXHUwMDE5XHUwMDAzXHUwMDE0XHUwMDAyXHJcdTAwMTLcafLfWcv/NvQ3jMcoxGOAK8w8a5M0299cdTAwMDexXGZdpJZKXHUwMDE4alx1MDAxYYTF/q6uij/+ovF2dU8ovlx0/PhcdTAwMGb1JtjE55ycXHUwMDAxP9VSY52k/VXteVNcdTAwMWXWVT7zRKKr71x1MDAwMD9cdTAwMTh8WDfgOFx1MDAxMeiaXGLj1bC7T3YqXHUwMDE011x1MDAxY1wi+0Dgx8lcdTAwMTFcZugtoDdcdTAwMDFXXHUwMDA27K8xxJoqXHUwMDBmmm96pfTO6aljiKZcdTAwMDFIpFwiXHUwMDAywE07eJB+fFXqOCTdqWNcdTAwMWQwfDhYaXUoe0xcdMVEXG52eqHV4fPDXHUwMDAwg4ThJ7ZMmMFMwy2YnVx1MDAwZqfcmlvUTKsw/1x1MDAxMEkv649iqIG43MdcdTAwMGbqNUeFOVPVv1x1MDAxMURgTNhNXHUwMDEyM+/ac7hkXHUwMDAxXHUwMDE2RDrtUDr7Usun7U0hOVx1MDAxZSR3VeTqOLx80Ph6XHUwMDE4r4cqyZjs7TKJUbipn+qt3WVVUNRf4k0lu2HJO/1AVS2jg1L6ZbGNyGZyVVx1MDAxObw+7oZ+042DJfDg6FhmWlxmUZSBR8jclFx1MDAxN0Ixw3ihXHUwMDAxXHUwMDAyOKArulFvk8NDOdTdgVx1MDAwNIL4XHUwMDAwqVx1MDAxYmdcIj9Vw5ToLlx1MDAxZZzf7atcdTAwMDZcdPunwLD/gFNsiOXA9zuj4/6wilx1MDAwNlnCKVx1MDAxMVx1MDAwNyBdUm79pKc2RLZSXcZ92WrqmlBcdTAwMWWQzkCPhOHaoIJcdTAwMTl6XHUwMDExj1xmXHUwMDE4hvCCXHRcdTAwMDLPhVDNsdiXf1x1MDAwZfppSrCC4TOeXFwk9FZdwKXUXHUwMDEw2Fx1MDAwYrB4VO2n4j/rIT4mNkIlwt1ens2ysUHkuZKLXHUwMDEyn01cdTAwMTiIXHUwMDAzk0m1UEJ4ea3u41x1MDAwMM5uXHUwMDFjbob8pcK89uQhWO7kgVx1MDAwMX3SfyxcdTAwMDfIr6E+l6eb1p4q2nL80qzGdqtB5ul1to32XHUwMDEz31x1MDAwMngg4LS4XHUwMDA06ylcdTAwMTXYLK7dptWcQVvCpOY1oaBcdTAwMTJcdTAwMTeyffhcdTAwMDFcdTAwMGYm1ptwSayUpICAXHUwMDFlwEOkhVx1MDAxODf8Mlx1MDAwNl8w32PdNeSH5uQ2XHUwMDE4wJOqhKv3k1glw0dZMlx1MDAxOLZCqdZ9xUfnuVx1MDAwNJVHRGv4XtgxXHUwMDFl9MelVU0nXHUwMDAzXHUwMDBmxFx1MDAxM1xmXHUwMDExXCJcdTAwMDRRXG7UWFGuvWqrvlx1MDAxYXp85eLt8r5IfFx1MDAxN/BxMv5+XHUwMDAwXHUwMDFmbjJcdTAwMTGUnFx1MDAxMceGdu1SUm6eZqH7cmdlz8f1Sed7JLso0ZaCp8pcdTAwMTgzJMjSjT1KaFx1MDAwYq5cYoFcdTAwMTk3x/tcdTAwMTfyfPhgXHUwMDBm09hcdTAwMDLsY1xiXHUwMDE5ujGsvHxcdTAwMWWkLcPFxk1CSWnmsFx1MDAxY38n2lx1MDAwNTJeW0Cg51x1MDAxY3dcdTAwMDMzQHZwsVx1MDAwMX5cdTAwMTSWmnrkt5HJJoBXh8FcdTAwMDOQhH6y/CDxmFx1MDAxY45cdTAwMWaGo24+VOk1KrvGqL/R3lBkXGI7NOdcXHKJOXfVRr/n/s24KFx1MDAwNT5cdTAwMDcjXGJpdutcblx1MDAwNEBEXHUwMDBiXHUwMDAybmKIXHUwMDBlhSbUXHUwMDA1RMyMK1x1MDAwM8GQhlx1MDAwZlFxxY6+XHUwMDFkXGIziDagvuFm0Ew7341cdTAwMTDwXHUwMDAxkVRcdTAwMDRcdKreXHUwMDE4cI6+na9cZr9dXHUwMDE1wlwiILpcXFx1MDAwYlxuN1x1MDAwYoD0m6Ck83R9z0OTIFx1MDAxNlqS01Hy8FSjXHUwMDAwoyTB9MORXHUwMDAwNr1fnClcdTAwMTBcdTAwMGatQEfkbbwyw6khIORcdTAwMDT5JFx1MDAxMFx1MDAxNXGvelxiYlx1MDAxOHk5gVBcdTAwMWScXHUwMDA3+O8+P7w01ptcdFx1MDAxYYzCrHQ0VVx1MDAxZdYy4dhDt1x1MDAxYu9cclR02mBcdTAwMGJcdTAwMWZoRG+NoiBlmIi3yqt9XHUwMDE4kq5cdTAwMTNcdTAwMDTGP3kgeXion3tXXGa9RahcXCnQaOlxgoCFx7noVdHQVzDMK7QvXHUwMDEz31x1MDAwNG+I9HXKiGFcdTAwMTUh4pzppd4tsd54XHUwMDEzXHUwMDE46jXMKFx1MDAwMcsqwFx1MDAxM8DgXHUwMDE3OJNzv2awUIuCXHUwMDFiQI3tMmO3Llx1MDAwMZ6Do1x1MDAxYU2RXHJcdTAwMTLKXHUwMDEwXHUwMDA3XHUwMDEzYmqiPaZTIFx1MDAwYrBRMm3Gm5v4lbO901x1MDAwN/p2XGaIfKZTfDX65CbzUrbUKG+WrUZVjLKt1axcdTAwMWLyK0+XZta4cW1cYkeKeNSBXHUwMDBi95PC+y1rJ4HPYb6BXHUwMDBmkEhcdFx1MDAwNctLwE9cdTAwMTSm1GpcdTAwMWZ8sPXmu0ku3mbNy1x1MDAxYjtm/mJiXiFcdTAwMGZcdPkmYIRcdTAwMGbMyIGImLvn9lx1MDAxZZ3bdnAgY0DBiIDva5mRXHUwMDAzXHUwMDFhI8NcdTAwMDXJP4KRJNZbhVx1MDAxNuVcYmIvdFFS/FxiXHUwMDE4XHSIgTBnSGolMJZcdTAwMWUnQcQyx5wmc1x1MDAwZn6ZkpLvlahcdTAwMTPwvzV1+qzfpVx1MDAxOFx1MDAwYlx1MDAxYsJcdTAwMDWAfWRidSSkZ5CoTbuvaVx1MDAxMDFcdTAwMDOByCcz5ed4QlxiPCAlTf5AcMm4R76egJ9CtFx02CBCgtBcYqm9XV1cdTAwMTmN/OTEvEL7XCLyTcCISP++XkRcdTAwMTXEvvKMrr2moP2cPVx1MDAxZKJINb64f85cIlx1MDAxMc/Wg1x1MDAwZUZcdTAwMTgjXHUwMDA2loZgzZHp//xYpiU5sVxmvVx1MDAxY6ZcYuCZittccrHmmFgmXHUwMDExI5hhPeTKq+lXgFx1MDAxOVx1MDAwNndcdTAwMTVcdTAwMTRcdTAwMDMx5CxcdTAwMWX9OyqjXHUwMDE0XHUwMDAwUynvWUBfXHJFvXJ/lLibNsplla4nVoqlo5uRX/gjNCdcdTAwMWE8cVx1MDAxM/9cdTAwMTLtUUBcdTAwMDVcdTAwMWWq4zlIsa/zJyFRPoam5fBcdTAwMWSLZVwitq4sUrI7XfmkzsG1MKxcdTAwMDdG64VzLvhcdTAwMGZ3ibpcdTAwMTZcdTAwMTR8UXhcdTAwMWVcdTAwMWVbuipcZvlKiHmF9oTju4CQf8dcdTAwMWUzY064PGNC0+FcdTAwMDHUXHUwMDAxxSBihq4oSn9aYazEh8kvhjLJkpxcbqS4YVUkt/OIpIlcdTAwMTOFoIhziFx1MDAwNVx1MDAxNfNcdTAwMTjWZLp2QP1cZsOjlFx1MDAxNKP93mHTsYW0dFx1MDAwZSD+o7mhQkjFMqnyXHUwMDE2P9Zas5Emr21cdTAwMTL1gSFcdTAwMDG+KTLJVFxyTlx1MDAwZvNwiCg2Y+O4QJpcYsGEkJ+EoZNcdTAwMWRcItiTXHUwMDAw1Fx1MDAwMyeTI3DU4Mbvw5Bp51x1MDAwNFxmYlxcY27mXGLfODjzXHUwMDE1XHUwMDEy89pcdTAwMTePb4JDXHUwMDE0+XdcdTAwMGVD/ElccifE6Wlp70H3QVx1MDAwN1wiramlhTSdbmBi9YfAjDJ47kRcdTAwMTlcdTAwMWV/XHUwMDEzljmvXz0yg1x1MDAxOFBcdTAwMWGjakRcdTAwMWXie4/AXGZiXHUwMDA0i3FsmOBcdTAwMTWCpVx1MDAxZc5cdTAwMTBcdTAwMDa/XFxzXHUwMDFkXGZcdTAwMTg6Q+NcdTAwMDGAkDZTabTilHpQLYEzJFx1MDAxNJZKMIpcdFx1MDAwNNGfZFo6mf9cdLakXHUwMDA1klx1MDAxMmuBkVFujz1x8JMhnJemd1xuMa/zxOvGZH5cdTAwMTJiXnuy8U0wiDt49veyQ8owSpEzSK6965aDjkGcmkNgMMFIczC1bn5EbeZVXHUwMDEyMyxcdTAwMWOZ2VT8476uh0DgcYk3OjOIXHUwMDEwiaaIebGoUFx1MDAxMDOjgOBcdTAwMGWAXHUwMDA11sJjOiszc95lMCDonKJJZFx1MDAwMlxmwohcdTAwMTmSatq09jFIWKai0lx1MDAxY05rZEKlz2HQyfVUsCVccpqOKFhjXHUwMDBljlx1MDAxN/ZgMFBcdTAwMTZHXG6D1CAzYFx1MDAxYuFcdTAwMWJcdTAwMDdkIX8hMa998VxiXHUwMDE2XGb5Tl45kFx1MDAxNpJcYlxcZCfT2TFcdTAwMTDK9VuJXHUwMDA15lx1MDAwZjjaXHUwMDEwkWGnabPHZ7/z+cCAUIiZLlRuWDnMSFx1MDAxM8XdWSF4XHUwMDAzZlHJXHUwMDE5aIiZU3hcdFxi/ZctmPbiaNXSg/dcdTAwMTjLvWiLS82JYJ/J+XzBgKGWXHLw2rDn//r3qDbp/vjvXHUwMDFms0njXz86i8Vk/q9//rPdXXSWdasxXHUwMDFl/nNRXHUwMDFihFx1MDAxNnaj88/2OLSzZ+N/j3rj+tGV06W9tP89gj+Bdsw7R9e/rfLsk8BcdTAwMGVK3M/NdvnHL3X+qzaZXHUwMDE0XHUwMDE3tYX9XHUwMDFi9eCJdJu/bt670v616trrO/9Jyv8w7/n//j901fBEIn0= + + + + WebApplicationFirewallAppWebPCCDNDev TeamKubernetesAPIRPCETCDJobDocker RegistryGitLab CI RunnerLint & TestCodeKafkaFilebeatlog-pilotAlert & Reportgo-stashNginxBuild & Package & DeployBow - Deploy PipelineJenkinsServiceLoadBalancerMonitorGrafanaPrometheusElasticSearchKibanaRedisMySQLMongoDBDatabasesData WarehouseClickhouse Data SystemGitLabReferences:api & rpc: https://github.com/zeromicro/go-zerojob: https://github.com/tal-tech/go-queuego-stash: https://github.com/tal-tech/go-stash \ No newline at end of file diff --git a/go-zero.dev/cn/resource/author.jpeg b/go-zero.dev/cn/resource/author.jpeg new file mode 100644 index 00000000..c566910a Binary files /dev/null and b/go-zero.dev/cn/resource/author.jpeg differ diff --git a/go-zero.dev/cn/resource/b97bf7df-1781-436e-bf04-f1dd90c60537.png b/go-zero.dev/cn/resource/b97bf7df-1781-436e-bf04-f1dd90c60537.png new file mode 100644 index 00000000..4e4455a3 Binary files /dev/null and b/go-zero.dev/cn/resource/b97bf7df-1781-436e-bf04-f1dd90c60537.png differ diff --git a/go-zero.dev/cn/resource/biz-redis-01.svg b/go-zero.dev/cn/resource/biz-redis-01.svg new file mode 100644 index 00000000..95e80f43 --- /dev/null +++ b/go-zero.dev/cn/resource/biz-redis-01.svg @@ -0,0 +1,16 @@ + + + + + + + clientbiz cachedb查 list1增、改、删 list2同步db \ No newline at end of file diff --git a/go-zero.dev/cn/resource/biz-redis-02.svg b/go-zero.dev/cn/resource/biz-redis-02.svg new file mode 100644 index 00000000..acf355e1 --- /dev/null +++ b/go-zero.dev/cn/resource/biz-redis-02.svg @@ -0,0 +1,16 @@ + + + + + + + clientbiz cachedb增、删、查、改 list1查询 id = 1 的item2cache缓存未命中查询db回填缓存查询 id = 1 item3缓存命中 \ No newline at end of file diff --git a/go-zero.dev/cn/resource/book.zip b/go-zero.dev/cn/resource/book.zip new file mode 100644 index 00000000..a62c7bcd Binary files /dev/null and b/go-zero.dev/cn/resource/book.zip differ diff --git a/go-zero.dev/cn/resource/breaker_state.png b/go-zero.dev/cn/resource/breaker_state.png new file mode 100644 index 00000000..73ce5141 Binary files /dev/null and b/go-zero.dev/cn/resource/breaker_state.png differ diff --git a/go-zero.dev/cn/resource/c42c34e8d33d48ec8a63e56feeae882a.png b/go-zero.dev/cn/resource/c42c34e8d33d48ec8a63e56feeae882a.png new file mode 100644 index 00000000..1fdebc4b Binary files /dev/null and b/go-zero.dev/cn/resource/c42c34e8d33d48ec8a63e56feeae882a.png differ diff --git a/go-zero.dev/cn/resource/call_chain.png b/go-zero.dev/cn/resource/call_chain.png new file mode 100644 index 00000000..6a96a75a Binary files /dev/null and b/go-zero.dev/cn/resource/call_chain.png differ diff --git a/go-zero.dev/cn/resource/ci-cd.png b/go-zero.dev/cn/resource/ci-cd.png new file mode 100644 index 00000000..ee16b01e Binary files /dev/null and b/go-zero.dev/cn/resource/ci-cd.png differ diff --git a/go-zero.dev/cn/resource/client_rejection2.png b/go-zero.dev/cn/resource/client_rejection2.png new file mode 100644 index 00000000..11373680 Binary files /dev/null and b/go-zero.dev/cn/resource/client_rejection2.png differ diff --git a/go-zero.dev/cn/resource/clone.png b/go-zero.dev/cn/resource/clone.png new file mode 100644 index 00000000..e463d566 Binary files /dev/null and b/go-zero.dev/cn/resource/clone.png differ diff --git a/go-zero.dev/cn/resource/compare.png b/go-zero.dev/cn/resource/compare.png new file mode 100644 index 00000000..42216ae3 Binary files /dev/null and b/go-zero.dev/cn/resource/compare.png differ diff --git a/go-zero.dev/cn/resource/dc500acd526d40aabfe4f53cf5bd180a_tplv-k3u1fbpfcp-zoom-1.png b/go-zero.dev/cn/resource/dc500acd526d40aabfe4f53cf5bd180a_tplv-k3u1fbpfcp-zoom-1.png new file mode 100644 index 00000000..716ea11d Binary files /dev/null and b/go-zero.dev/cn/resource/dc500acd526d40aabfe4f53cf5bd180a_tplv-k3u1fbpfcp-zoom-1.png differ diff --git a/go-zero.dev/cn/resource/doc-edit.png b/go-zero.dev/cn/resource/doc-edit.png new file mode 100644 index 00000000..14b9eb10 Binary files /dev/null and b/go-zero.dev/cn/resource/doc-edit.png differ diff --git a/go-zero.dev/cn/resource/docker_env.png b/go-zero.dev/cn/resource/docker_env.png new file mode 100644 index 00000000..5ac04759 Binary files /dev/null and b/go-zero.dev/cn/resource/docker_env.png differ diff --git a/go-zero.dev/cn/resource/f93c621571074e44a2d403aa25e7db6f_tplv-k3u1fbpfcp-zoom-1.png b/go-zero.dev/cn/resource/f93c621571074e44a2d403aa25e7db6f_tplv-k3u1fbpfcp-zoom-1.png new file mode 100644 index 00000000..afcde0fb Binary files /dev/null and b/go-zero.dev/cn/resource/f93c621571074e44a2d403aa25e7db6f_tplv-k3u1fbpfcp-zoom-1.png differ diff --git a/go-zero.dev/cn/resource/fork.png b/go-zero.dev/cn/resource/fork.png new file mode 100644 index 00000000..44521bb3 Binary files /dev/null and b/go-zero.dev/cn/resource/fork.png differ diff --git a/go-zero.dev/cn/resource/fx_log.png b/go-zero.dev/cn/resource/fx_log.png new file mode 100644 index 00000000..45ca95e6 Binary files /dev/null and b/go-zero.dev/cn/resource/fx_log.png differ diff --git a/go-zero.dev/cn/resource/gitlab-git-url.png b/go-zero.dev/cn/resource/gitlab-git-url.png new file mode 100644 index 00000000..b2ed854a Binary files /dev/null and b/go-zero.dev/cn/resource/gitlab-git-url.png differ diff --git a/go-zero.dev/cn/resource/go-zero-logo.png b/go-zero.dev/cn/resource/go-zero-logo.png new file mode 100644 index 00000000..a0ec1cd5 Binary files /dev/null and b/go-zero.dev/cn/resource/go-zero-logo.png differ diff --git a/go-zero.dev/cn/resource/go-zero-practise.png b/go-zero.dev/cn/resource/go-zero-practise.png new file mode 100755 index 00000000..0be4da53 Binary files /dev/null and b/go-zero.dev/cn/resource/go-zero-practise.png differ diff --git a/go-zero.dev/cn/resource/go_live_template.png b/go-zero.dev/cn/resource/go_live_template.png new file mode 100644 index 00000000..28110974 Binary files /dev/null and b/go-zero.dev/cn/resource/go_live_template.png differ diff --git a/go-zero.dev/cn/resource/goctl-api-select.png b/go-zero.dev/cn/resource/goctl-api-select.png new file mode 100644 index 00000000..d852489f Binary files /dev/null and b/go-zero.dev/cn/resource/goctl-api-select.png differ diff --git a/go-zero.dev/cn/resource/goctl-api.png b/go-zero.dev/cn/resource/goctl-api.png new file mode 100644 index 00000000..83a5cf48 Binary files /dev/null and b/go-zero.dev/cn/resource/goctl-api.png differ diff --git a/go-zero.dev/cn/resource/goctl-command.png b/go-zero.dev/cn/resource/goctl-command.png new file mode 100644 index 00000000..6f38d223 Binary files /dev/null and b/go-zero.dev/cn/resource/goctl-command.png differ diff --git a/go-zero.dev/cn/resource/grafana-app.png b/go-zero.dev/cn/resource/grafana-app.png new file mode 100644 index 00000000..aa97e4d5 Binary files /dev/null and b/go-zero.dev/cn/resource/grafana-app.png differ diff --git a/go-zero.dev/cn/resource/grafana-panel.png b/go-zero.dev/cn/resource/grafana-panel.png new file mode 100644 index 00000000..b82430c5 Binary files /dev/null and b/go-zero.dev/cn/resource/grafana-panel.png differ diff --git a/go-zero.dev/cn/resource/grafana-qps.png b/go-zero.dev/cn/resource/grafana-qps.png new file mode 100644 index 00000000..14a86dd5 Binary files /dev/null and b/go-zero.dev/cn/resource/grafana-qps.png differ diff --git a/go-zero.dev/cn/resource/grafana.png b/go-zero.dev/cn/resource/grafana.png new file mode 100644 index 00000000..0c648728 Binary files /dev/null and b/go-zero.dev/cn/resource/grafana.png differ diff --git a/go-zero.dev/cn/resource/handler.gif b/go-zero.dev/cn/resource/handler.gif new file mode 100644 index 00000000..fd1c1c38 Binary files /dev/null and b/go-zero.dev/cn/resource/handler.gif differ diff --git a/go-zero.dev/cn/resource/info.gif b/go-zero.dev/cn/resource/info.gif new file mode 100644 index 00000000..f4c26bf5 Binary files /dev/null and b/go-zero.dev/cn/resource/info.gif differ diff --git a/go-zero.dev/cn/resource/intellij-model.png b/go-zero.dev/cn/resource/intellij-model.png new file mode 100644 index 00000000..66dd40ad Binary files /dev/null and b/go-zero.dev/cn/resource/intellij-model.png differ diff --git a/go-zero.dev/cn/resource/interceptor.png b/go-zero.dev/cn/resource/interceptor.png new file mode 100644 index 00000000..c4899132 Binary files /dev/null and b/go-zero.dev/cn/resource/interceptor.png differ diff --git a/go-zero.dev/cn/resource/jenkins-add-credentials.png b/go-zero.dev/cn/resource/jenkins-add-credentials.png new file mode 100644 index 00000000..e5dc389d Binary files /dev/null and b/go-zero.dev/cn/resource/jenkins-add-credentials.png differ diff --git a/go-zero.dev/cn/resource/jenkins-build-with-parameters.png b/go-zero.dev/cn/resource/jenkins-build-with-parameters.png new file mode 100644 index 00000000..14684389 Binary files /dev/null and b/go-zero.dev/cn/resource/jenkins-build-with-parameters.png differ diff --git a/go-zero.dev/cn/resource/jenkins-choice.png b/go-zero.dev/cn/resource/jenkins-choice.png new file mode 100644 index 00000000..c86e722a Binary files /dev/null and b/go-zero.dev/cn/resource/jenkins-choice.png differ diff --git a/go-zero.dev/cn/resource/jenkins-configure.png b/go-zero.dev/cn/resource/jenkins-configure.png new file mode 100644 index 00000000..69a0b430 Binary files /dev/null and b/go-zero.dev/cn/resource/jenkins-configure.png differ diff --git a/go-zero.dev/cn/resource/jenkins-credentials-id.png b/go-zero.dev/cn/resource/jenkins-credentials-id.png new file mode 100644 index 00000000..2f633628 Binary files /dev/null and b/go-zero.dev/cn/resource/jenkins-credentials-id.png differ diff --git a/go-zero.dev/cn/resource/jenkins-credentials.png b/go-zero.dev/cn/resource/jenkins-credentials.png new file mode 100644 index 00000000..372b9262 Binary files /dev/null and b/go-zero.dev/cn/resource/jenkins-credentials.png differ diff --git a/go-zero.dev/cn/resource/jenkins-git.png b/go-zero.dev/cn/resource/jenkins-git.png new file mode 100644 index 00000000..950fc5b6 Binary files /dev/null and b/go-zero.dev/cn/resource/jenkins-git.png differ diff --git a/go-zero.dev/cn/resource/jenkins-new-item.png b/go-zero.dev/cn/resource/jenkins-new-item.png new file mode 100644 index 00000000..4ad817b6 Binary files /dev/null and b/go-zero.dev/cn/resource/jenkins-new-item.png differ diff --git a/go-zero.dev/cn/resource/json_tag.png b/go-zero.dev/cn/resource/json_tag.png new file mode 100644 index 00000000..e8f44c9c Binary files /dev/null and b/go-zero.dev/cn/resource/json_tag.png differ diff --git a/go-zero.dev/cn/resource/jump.gif b/go-zero.dev/cn/resource/jump.gif new file mode 100644 index 00000000..8581aae5 Binary files /dev/null and b/go-zero.dev/cn/resource/jump.gif differ diff --git a/go-zero.dev/cn/resource/k8s-01.png b/go-zero.dev/cn/resource/k8s-01.png new file mode 100644 index 00000000..9192926a Binary files /dev/null and b/go-zero.dev/cn/resource/k8s-01.png differ diff --git a/go-zero.dev/cn/resource/k8s-02.png b/go-zero.dev/cn/resource/k8s-02.png new file mode 100644 index 00000000..2ec67d6d Binary files /dev/null and b/go-zero.dev/cn/resource/k8s-02.png differ diff --git a/go-zero.dev/cn/resource/k8s-03.png b/go-zero.dev/cn/resource/k8s-03.png new file mode 100644 index 00000000..e672db3a Binary files /dev/null and b/go-zero.dev/cn/resource/k8s-03.png differ diff --git a/go-zero.dev/cn/resource/live_template.gif b/go-zero.dev/cn/resource/live_template.gif new file mode 100644 index 00000000..dac3499e Binary files /dev/null and b/go-zero.dev/cn/resource/live_template.gif differ diff --git a/go-zero.dev/cn/resource/log-flow.png b/go-zero.dev/cn/resource/log-flow.png new file mode 100644 index 00000000..2421ddce Binary files /dev/null and b/go-zero.dev/cn/resource/log-flow.png differ diff --git a/go-zero.dev/cn/resource/log.png b/go-zero.dev/cn/resource/log.png new file mode 100644 index 00000000..9c751d7e Binary files /dev/null and b/go-zero.dev/cn/resource/log.png differ diff --git a/go-zero.dev/cn/resource/logo.png b/go-zero.dev/cn/resource/logo.png new file mode 100644 index 00000000..16798ee5 Binary files /dev/null and b/go-zero.dev/cn/resource/logo.png differ diff --git a/go-zero.dev/cn/resource/new_pr.png b/go-zero.dev/cn/resource/new_pr.png new file mode 100644 index 00000000..02d38b9f Binary files /dev/null and b/go-zero.dev/cn/resource/new_pr.png differ diff --git a/go-zero.dev/cn/resource/pipeline.png b/go-zero.dev/cn/resource/pipeline.png new file mode 100644 index 00000000..eb51eaec Binary files /dev/null and b/go-zero.dev/cn/resource/pipeline.png differ diff --git a/go-zero.dev/cn/resource/pr_record.png b/go-zero.dev/cn/resource/pr_record.png new file mode 100644 index 00000000..8b0e4937 Binary files /dev/null and b/go-zero.dev/cn/resource/pr_record.png differ diff --git a/go-zero.dev/cn/resource/project_generate_code.png b/go-zero.dev/cn/resource/project_generate_code.png new file mode 100644 index 00000000..a637403b Binary files /dev/null and b/go-zero.dev/cn/resource/project_generate_code.png differ diff --git a/go-zero.dev/cn/resource/prometheus-flow.png b/go-zero.dev/cn/resource/prometheus-flow.png new file mode 100644 index 00000000..ce6a97af Binary files /dev/null and b/go-zero.dev/cn/resource/prometheus-flow.png differ diff --git a/go-zero.dev/cn/resource/prometheus-graph.webp b/go-zero.dev/cn/resource/prometheus-graph.webp new file mode 100644 index 00000000..d283fc5a Binary files /dev/null and b/go-zero.dev/cn/resource/prometheus-graph.webp differ diff --git a/go-zero.dev/cn/resource/prometheus-start.png b/go-zero.dev/cn/resource/prometheus-start.png new file mode 100644 index 00000000..ee0bbefa Binary files /dev/null and b/go-zero.dev/cn/resource/prometheus-start.png differ diff --git a/go-zero.dev/cn/resource/psiTree.png b/go-zero.dev/cn/resource/psiTree.png new file mode 100644 index 00000000..06af982a Binary files /dev/null and b/go-zero.dev/cn/resource/psiTree.png differ diff --git a/go-zero.dev/cn/resource/redis-cache-01.png b/go-zero.dev/cn/resource/redis-cache-01.png new file mode 100644 index 00000000..f07a1133 Binary files /dev/null and b/go-zero.dev/cn/resource/redis-cache-01.png differ diff --git a/go-zero.dev/cn/resource/redis-cache-02.png b/go-zero.dev/cn/resource/redis-cache-02.png new file mode 100644 index 00000000..ba8f2fb0 Binary files /dev/null and b/go-zero.dev/cn/resource/redis-cache-02.png differ diff --git a/go-zero.dev/cn/resource/redis-cache-03.png b/go-zero.dev/cn/resource/redis-cache-03.png new file mode 100644 index 00000000..8b2449a8 Binary files /dev/null and b/go-zero.dev/cn/resource/redis-cache-03.png differ diff --git a/go-zero.dev/cn/resource/redis-cache-04.png b/go-zero.dev/cn/resource/redis-cache-04.png new file mode 100644 index 00000000..38b3f0f4 Binary files /dev/null and b/go-zero.dev/cn/resource/redis-cache-04.png differ diff --git a/go-zero.dev/cn/resource/redis-cache-05.png b/go-zero.dev/cn/resource/redis-cache-05.png new file mode 100644 index 00000000..4a743ec7 Binary files /dev/null and b/go-zero.dev/cn/resource/redis-cache-05.png differ diff --git a/go-zero.dev/cn/resource/redis-cache-06.png b/go-zero.dev/cn/resource/redis-cache-06.png new file mode 100644 index 00000000..ec9cf88f Binary files /dev/null and b/go-zero.dev/cn/resource/redis-cache-06.png differ diff --git a/go-zero.dev/cn/resource/redis-cache-07.png b/go-zero.dev/cn/resource/redis-cache-07.png new file mode 100644 index 00000000..c84d292d Binary files /dev/null and b/go-zero.dev/cn/resource/redis-cache-07.png differ diff --git a/go-zero.dev/cn/resource/redis-cache-08.png b/go-zero.dev/cn/resource/redis-cache-08.png new file mode 100644 index 00000000..039816e4 Binary files /dev/null and b/go-zero.dev/cn/resource/redis-cache-08.png differ diff --git a/go-zero.dev/cn/resource/redis-cache-09.webp b/go-zero.dev/cn/resource/redis-cache-09.webp new file mode 100644 index 00000000..e13122b6 Binary files /dev/null and b/go-zero.dev/cn/resource/redis-cache-09.webp differ diff --git a/go-zero.dev/cn/resource/redis-cache-10.png b/go-zero.dev/cn/resource/redis-cache-10.png new file mode 100644 index 00000000..ec8e69a3 Binary files /dev/null and b/go-zero.dev/cn/resource/redis-cache-10.png differ diff --git a/go-zero.dev/cn/resource/redis-cache-11.webp b/go-zero.dev/cn/resource/redis-cache-11.webp new file mode 100644 index 00000000..5476baf0 Binary files /dev/null and b/go-zero.dev/cn/resource/redis-cache-11.webp differ diff --git a/go-zero.dev/cn/resource/service.gif b/go-zero.dev/cn/resource/service.gif new file mode 100644 index 00000000..dbf7f613 Binary files /dev/null and b/go-zero.dev/cn/resource/service.gif differ diff --git a/go-zero.dev/cn/resource/service.png b/go-zero.dev/cn/resource/service.png new file mode 100644 index 00000000..09b51d7f Binary files /dev/null and b/go-zero.dev/cn/resource/service.png differ diff --git a/go-zero.dev/cn/resource/ssh-add-key.png b/go-zero.dev/cn/resource/ssh-add-key.png new file mode 100644 index 00000000..70635b7e Binary files /dev/null and b/go-zero.dev/cn/resource/ssh-add-key.png differ diff --git a/go-zero.dev/cn/resource/type.gif b/go-zero.dev/cn/resource/type.gif new file mode 100644 index 00000000..e4d4d7a1 Binary files /dev/null and b/go-zero.dev/cn/resource/type.gif differ diff --git a/go-zero.dev/cn/resource/user-pipeline-script.png b/go-zero.dev/cn/resource/user-pipeline-script.png new file mode 100644 index 00000000..57ede338 Binary files /dev/null and b/go-zero.dev/cn/resource/user-pipeline-script.png differ diff --git a/go-zero.dev/cn/route-naming-spec.md b/go-zero.dev/cn/route-naming-spec.md new file mode 100644 index 00000000..53259d70 --- /dev/null +++ b/go-zero.dev/cn/route-naming-spec.md @@ -0,0 +1,10 @@ +# 路由规范 +* 推荐脊柱式命名 +* 小写单词、横杠(-)组合 +* 见名知义 + +```go +/user/get-info +/user/get/info +/user/password/change/:id +``` \ No newline at end of file diff --git a/go-zero.dev/cn/rpc-call.md b/go-zero.dev/cn/rpc-call.md new file mode 100644 index 00000000..1245d433 --- /dev/null +++ b/go-zero.dev/cn/rpc-call.md @@ -0,0 +1,252 @@ +# rpc编写与调用 +在一个大的系统中,多个子系统(服务)间必然存在数据传递,有数据传递就需要通信方式,你可以选择最简单的http进行通信,也可以选择rpc服务进行通信, +在go-zero,我们使用zrpc来进行服务间的通信,zrpc是基于grpc。 + +## 场景 +在前面我们完善了对用户进行登录,用户查询图书等接口协议,但是用户在查询图书时没有做任何用户校验,如果当前用户是一个不存在的用户则我们不允许其查阅图书信息, +从上文信息我们可以得知,需要user服务提供一个方法来获取用户信息供search服务使用,因此我们就需要创建一个user rpc服务,并提供一个getUser方法。 + +## rpc服务编写 + +* 编译proto文件 + ```shell + $ vim service/user/cmd/rpc/user.proto + ``` + ```protobuf + syntax = "proto3"; + + package user; + + option go_package = "user"; + + message IdReq{ + int64 id = 1; + } + + message UserInfoReply{ + int64 id = 1; + string name = 2; + string number = 3; + string gender = 4; + } + + service user { + rpc getUser(IdReq) returns(UserInfoReply); + } + ``` + * 生成rpc服务代码 + ```shell + $ cd service/user/cmd/rpc + $ goctl rpc proto -src user.proto -dir . + ``` +> [!TIPS] +> 如果安装的 `protoc-gen-go` 版大于1.4.0, proto文件建议加上`go_package` + +* 添加配置及完善yaml配置项 + ```shell + $ vim service/user/cmd/rpc/internal/config/config.go + ``` + ```go + type Config struct { + zrpc.RpcServerConf + Mysql struct { + DataSource string + } + CacheRedis cache.CacheConf + } + ``` + ```shell + $ vim /service/user/cmd/rpc/etc/user.yaml + ``` + ```yaml + Name: user.rpc + ListenOn: 127.0.0.1:8080 + Etcd: + Hosts: + - $etcdHost + Key: user.rpc + 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密码 + > + > $etcdHost: etcd连接地址,格式:ip:port,如: 127.0.0.1:2379 + > + > 更多配置信息,请参考[rpc配置介绍](rpc-config.md) + +* 添加资源依赖 + ```shell + $ vim service/user/cmd/rpc/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), + } + } + ``` +* 添加rpc逻辑 + ```shell + $ service/user/cmd/rpc/internal/logic/getuserlogic.go + ``` + ```go + func (l *GetUserLogic) GetUser(in *user.IdReq) (*user.UserInfoReply, error) { + one, err := l.svcCtx.UserModel.FindOne(in.Id) + if err != nil { + return nil, err + } + + return &user.UserInfoReply{ + Id: one.Id, + Name: one.Name, + Number: one.Number, + Gender: one.Gender, + }, nil + } + ``` + +## 使用rpc +接下来我们在search服务中调用user rpc + +* 添加UserRpc配置及yaml配置项 + ```shell + $ vim service/search/cmd/api/internal/config/config.go + ``` + ```go + type Config struct { + rest.RestConf + Auth struct { + AccessSecret string + AccessExpire int64 + } + UserRpc zrpc.RpcClientConf + } + ``` + ```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 + UserRpc: + Etcd: + Hosts: + - $etcdHost + Key: user.rpc + ``` + > [!TIP] + > $AccessSecret:这个值必须要和user api中声明的一致。 + > + > $AccessExpire: 有效期 + > + > $etcdHost: etcd连接地址 + > + > etcd中的`Key`必须要和user rpc服务配置中Key一致 +* 添加依赖 + ```shell + $ vim service/search/cmd/api/internal/svc/servicecontext.go + ``` + ```go + type ServiceContext struct { + Config config.Config + Example rest.Middleware + UserRpc userclient.User + } + + func NewServiceContext(c config.Config) *ServiceContext { + return &ServiceContext{ + Config: c, + Example: middleware.NewExampleMiddleware().Handle, + UserRpc: userclient.NewUser(zrpc.MustNewClient(c.UserRpc)), + } + } + ``` +* 补充逻辑 + ```shell + $ vim /service/search/cmd/api/internal/logic/searchlogic.go + ``` + ```go + func (l *SearchLogic) Search(req types.SearchReq) (*types.SearchReply, error) { + userIdNumber := json.Number(fmt.Sprintf("%v", l.ctx.Value("userId"))) + logx.Infof("userId: %s", userIdNumber) + userId, err := userIdNumber.Int64() + if err != nil { + return nil, err + } + + // 使用user rpc + _, err = l.svcCtx.UserRpc.GetUser(l.ctx, &userclient.IdReq{ + Id: userId, + }) + if err != nil { + return nil, err + } + + return &types.SearchReply{ + Name: req.Name, + Count: 100, + }, nil + } + ``` +## 启动并验证服务 +* 启动etcd、redis、mysql +* 启动user rpc + ```shell + $ cd /service/user/cmd/rpc + $ go run user.go -f etc/user.yaml + ``` + ```text + Starting rpc server at 127.0.0.1:8080... + ``` +* 启动search api +```shell +$ cd service/search/cmd/api +$ go run search.go -f etc/search-api.yaml +``` + +* 验证服务 + ```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: Tue, 09 Feb 2021 06:05:52 GMT + Content-Length: 32 + + {"name":"西游记","count":100} + ``` + +# 猜你想看 +* [rpc配置](rpc-config.md) +* [rpc服务目录](rpc-dir.md) +* [goctl rpc命令](goctl-rpc.md) diff --git a/go-zero.dev/cn/rpc-config.md b/go-zero.dev/cn/rpc-config.md new file mode 100644 index 00000000..8836b878 --- /dev/null +++ b/go-zero.dev/cn/rpc-config.md @@ -0,0 +1,52 @@ +# rpc配置 + +rpc配置控制着一个rpc服务的各种功能,包含但不限于监听地址,etcd配置,超时,熔断配置等,下面我们以一个常见的rpc服务配置来进行说明。 + +## 配置说明 +```go +Config struct { + zrpc.RpcServerConf + CacheRedis cache.CacheConf // redis缓存配置,详情见api配置说明,这里不赘述 + Mysql struct { // mysql数据库访问配置,详情见api配置说明,这里不赘述 + DataSource string + } +} +``` + +### zrpc.RpcServerConf +```go +RpcServerConf struct { + service.ServiceConf // 服务配置,详情见api配置说明,这里不赘述 + ListenOn string // rpc监听地址和端口,如:127.0.0.1:8888 + Etcd discov.EtcdConf `json:",optional"` // etcd相关配置 + Auth bool `json:",optional"` // 是否开启Auth,如果是则Redis为必填 + Redis redis.RedisKeyConf `json:",optional"` // Auth验证 + StrictControl bool `json:",optional"` // 是否Strict模式,如果是则遇到错误是Auth失败,否则可以认为成功 + // pending forever is not allowed + // never set it to 0, if zero, the underlying will set to 2s automatically + Timeout int64 `json:",default=2000"` // 超时控制,单位:毫秒 + CpuThreshold int64 `json:",default=900,range=[0:1000]"` cpu降载阈值,默认900,可允许设置范围0到1000 +} +``` + +### discov.EtcdConf +```go +type EtcdConf struct { + Hosts []string // etcd host数组 + Key string // rpc注册key +} +``` + +### redis.RedisKeyConf +```go +RedisConf struct { + Host string // redis 主机 + Type string `json:",default=node,options=node|cluster"` // redis类型 + Pass string `json:",optional"` // redis密码 +} + +RedisKeyConf struct { + RedisConf + Key string `json:",optional"` // 验证key +} +``` diff --git a/go-zero.dev/cn/rpc-dir.md b/go-zero.dev/cn/rpc-dir.md new file mode 100644 index 00000000..e8641673 --- /dev/null +++ b/go-zero.dev/cn/rpc-dir.md @@ -0,0 +1,49 @@ +# rpc服务目录 + +```text +. +├── etc // yaml配置文件 +│ └── greet.yaml +├── go.mod +├── greet // pb.go文件夹① +│ └── greet.pb.go +├── greet.go // main函数 +├── greet.proto // proto 文件 +├── greetclient // call logic ② +│ └── greet.go +└── internal + ├── config // yaml配置对应的实体 + │ └── config.go + ├── logic // 业务代码 + │ └── pinglogic.go + ├── server // rpc server + │ └── greetserver.go + └── svc // 依赖资源 + └── servicecontext.go +``` + +> [!TIP] +> ① pb文件夹名(老版本文件夹固定为pb)称取自于proto文件中option go_package的值最后一层级按照一定格式进行转换,若无此声明,则取自于package的值,大致代码如下: + +```go + if option.Name == "go_package" { + ret.GoPackage = option.Constant.Source + } + ... + if len(ret.GoPackage) == 0 { + ret.GoPackage = ret.Package.Name + } + ret.PbPackage = GoSanitized(filepath.Base(ret.GoPackage)) + ... +``` +> [!TIP] +> GoSanitized方法请参考google.golang.org/protobuf@v1.25.0/internal/strs/strings.go:71 + +> [!TIP] +> ② call 层文件夹名称取自于proto中service的名称,如该sercice的名称和pb文件夹名称相等,则会在srervice后面补充client进行区分,使pb和call分隔。 + +```go +if strings.ToLower(proto.Service.Name) == strings.ToLower(proto.GoPackage) { + callDir = filepath.Join(ctx.WorkDir, strings.ToLower(stringx.From(proto.Service.Name+"_client").ToCamel())) +} +``` \ No newline at end of file diff --git a/go-zero.dev/cn/service-deployment.md b/go-zero.dev/cn/service-deployment.md new file mode 100644 index 00000000..b411d5be --- /dev/null +++ b/go-zero.dev/cn/service-deployment.md @@ -0,0 +1,241 @@ +# 服务部署 +本节通过jenkins来进行简单的服务部署到k8s演示。 + +## 准备工作 +* k8s集群安装 +* gitlab环境安装 +* jenkins环境安装 +* redis&mysql&nginx&etcd安装 +* [goctl安装](goctl-install.md) + +> [!TIP] +> goctl确保k8s每个node节点上都有 +> +> 以上环境安装请自行google,这里不做篇幅介绍。 + +## 服务部署 +### 1、gitlab代码仓库相关准备 + +#### 1.1、添加SSH Key + +进入gitlab,点击用户中心,找到`Settings`,在左侧找到`SSH Keys`tab +![ssh key](./resource/ssh-add-key.png) + +* 1、在jenkins所在机器上查看公钥 + +```shell +$ cat ~/.ssh/id_rsa.pub +``` + +* 2、如果没有,则需要生成,如果存在,请跳转到第3步 + +```shell +$ ssh-keygen -t rsa -b 2048 -C "email@example.com" +``` + +> "email@example.com" 可以替换为自己的邮箱 +> +完成生成后,重复第一步操作 + +* 3、将公钥添加到gitlab中 + +#### 1.2、上传代码到gitlab仓库 +新建工程`go-zero-demo`并上传代码,这里不做细节描述。 + +### 2、jenkins + +#### 2.1、添加凭据 + +* 查看jenkins所在机器的私钥,与前面gitlab公钥对应 + +```shell +$ cat id_rsa +``` + +* 进入jenkins,依次点击`Manage Jenkins`-> `Manage Credentials` + ![credentials](./resource/jenkins-credentials.png) + +* 进入`全局凭据`页面,添加凭据,`Username`是一个标识,后面添加pipeline你知道这个标识是代表gitlab的凭据就行,Private Key`即上面获取的私钥 + ![jenkins-add-credentials](./resource/jenkins-add-credentials.png) + +#### 2.2、 添加全局变量 +进入`Manage Jenkins`->`Configure System`,滑动到`全局属性`条目,添加docker私有仓库相关信息,如图为`docker用户名`、`docker用户密码`、`docker私有仓库地址` +![docker_server](./resource/docker_env.png) + +> [!TIP] +> +> `docker_user` 修改为你的docker用户名 +> +> `docker_pass` 修改为你的docker用户密码 +> +> `docker_server` 修改为你的docker服务器地址 +> +> 这里我使用的私有仓库,如果没有云厂商提供的私有仓库使用,可以自行搭建一个私有仓库,这里就不赘述了,大家自行google。 + +#### 2.3、配置git +进入`Manage Jenkins`->`Global Tool Configureation`,找到Git条目,填写jenkins所在机器git可执行文件所在path,如果没有的话,需要在jenkins插件管理中下载Git插件。 +![jenkins-git](./resource/jenkins-git.png) + + +![jenkins-configure](./resource/jenkins-configure.png) +#### 2.4、 添加一个Pipeline + +> pipeline用于构建项目,从gitlab拉取代码->生成Dockerfile->部署到k8s均在这个步骤去做,这里是演示环境,为了保证部署流程顺利, +> 需要将jenkins安装在和k8s集群的其中过一个节点所在机器上,我这里安装在master上的。 + +* 获取凭据id 进入凭据页面,找到Username为`gitlab`的凭据id + ![jenkins-credentials-id](./resource/jenkins-credentials-id.png) + +* 进入jenkins首页,点击`新建Item`,名称为`user` + ![jenkins-add-item](./resource/jenkins-new-item.png) + +* 查看项目git地址 + ![gitlab-git-url](./resource/gitlab-git-url.png) + +* 添加服务类型Choice Parameter,在General中勾选`This project is parameterized + `,点击`添加参数`选择`Choice Parameter`,按照图中添加选择的值常量(api、rpc)及接收值的变量(type),后续在Pipeline script中会用到。 + ![jenkins-choice-parameter](./resource/jenkins-choice.png) + +* 配置`user`,在`user`配置页面,向下滑动找到`Pipeline script`,填写脚本内容 + +```text +pipeline { + agent any + parameters { + gitParameter name: 'branch', + type: 'PT_BRANCH', + branchFilter: 'origin/(.*)', + defaultValue: 'master', + selectedValue: 'DEFAULT', + sortMode: 'ASCENDING_SMART', + description: '选择需要构建的分支' + } + + stages { + stage('服务信息') { + steps { + sh 'echo 分支:$branch' + sh 'echo 构建服务类型:${JOB_NAME}-$type' + } + } + + + stage('check out') { + steps { + checkout([$class: 'GitSCM', + branches: [[name: '$branch']], + doGenerateSubmoduleConfigurations: false, + extensions: [], + submoduleCfg: [], + userRemoteConfigs: [[credentialsId: '${credentialsId}', url: '${gitUrl}']]]) + } + } + + stage('获取commit_id') { + steps { + echo '获取commit_id' + git credentialsId: '${credentialsId}', url: '${gitUrl}' + script { + env.commit_id = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim() + } + } + } + + + stage('goctl版本检测') { + steps{ + sh '/usr/local/bin/goctl -v' + } + } + + stage('Dockerfile Build') { + steps{ + sh '/usr/local/bin/goctl docker -go service/${JOB_NAME}/${type}/${JOB_NAME}.go' + script{ + env.image = sh(returnStdout: true, script: 'echo ${JOB_NAME}-${type}:${commit_id}').trim() + } + sh 'echo 镜像名称:${image}' + sh 'docker build -t ${image} .' + } + } + + stage('上传到镜像仓库') { + steps{ + sh '/root/dockerlogin.sh' + sh 'docker tag ${image} ${dockerServer}/${image}' + sh 'docker push ${dockerServer}/${image}' + } + } + + stage('部署到k8s') { + steps{ + script{ + env.deployYaml = sh(returnStdout: true, script: 'echo ${JOB_NAME}-${type}-deploy.yaml').trim() + env.port=sh(returnStdout: true, script: '/root/port.sh ${JOB_NAME}-${type}').trim() + } + + sh 'echo ${port}' + + sh 'rm -f ${deployYaml}' + sh '/usr/local/bin/goctl kube deploy -secret dockersecret -replicas 2 -nodePort 3${port} -requestCpu 200 -requestMem 50 -limitCpu 300 -limitMem 100 -name ${JOB_NAME}-${type} -namespace hey-go-zero -image ${dockerServer}/${image} -o ${deployYaml} -port ${port}' + sh '/usr/bin/kubectl apply -f ${deployYaml}' + } + } + + stage('Clean') { + steps{ + sh 'docker rmi -f ${image}' + sh 'docker rmi -f ${dockerServer}/${image}' + cleanWs notFailBuild: true + } + } + } +} +``` + +> [!TIP] +> ${credentialsId}要替换为你的具体凭据值,即【添加凭据】模块中的一串字符串,${gitUrl}需要替换为你代码的git仓库地址,其他的${xxx}形式的变量无需修改,保持原样即可。 +> ![user-pipepine-script](./resource/user-pipeline-script.png) + +### port.sh参考内容如下 +``` +case $1 in +"user-api") echo 1000 +;; +"user-rpc") echo 1001 +;; +"course-api") echo 1002 +;; +"course-rpc") echo 1003 +;; +"selection-api") echo 1004 +esac +``` + +其中dockerlogin.sh内容 + +```shell +#!/bin/bash +docker login --username=$docker-user --password=$docker-pass $docker-server +``` + +* $docker-user: docker登录用户名 +* $docker-pass: docker登录用户密码 +* $docker-server: docker私有地址 + + +## 查看pipeline +![build with parameters](./resource/jenkins-build-with-parameters.png) +![build with parameters](./resource/pipeline.png) + +## 查看k8s服务 +![k8s01](./resource/k8s-01.png) + +# 猜你想看 +* [goctl安装](goctl-install.md) +* [k8s介绍](https://kubernetes.io/zh/) +* [docker介绍](https://www.docker.com/) +* [jenkins安装](https://www.jenkins.io/zh/doc/book/installing/) +* [jenkins pipeline](https://www.jenkins.io/zh/doc/pipeline/tour/hello-world/) +* [nginx文档介绍](http://nginx.org/en/docs/) +* [etcd文档说明](https://etcd.io/docs/current/) \ No newline at end of file diff --git a/go-zero.dev/cn/service-design.md b/go-zero.dev/cn/service-design.md new file mode 100644 index 00000000..d1805335 --- /dev/null +++ b/go-zero.dev/cn/service-design.md @@ -0,0 +1,88 @@ +# 目录拆分 +目录拆分是指配合go-zero的最佳实践的目录拆分,这和微服务拆分有着关联,在团队内部最佳实践中, +我们按照业务横向拆分,将一个系统拆分成多个子系统,每个子系统应拥有独立的持久化存储,缓存系统。 +如一个商城系统需要有用户系统(user),商品管理系统(product),订单系统(order),购物车系统(cart),结算中心系统(pay),售后系统(afterSale)等组成。 + +## 系统结构分析 +在上文提到的商城系统中,每个系统在对外(http)提供服务的同时,也会提供数据给其他子系统进行数据访问的接口(rpc),因此每个子系统可以拆分成一个服务,而且对外提供了两种访问该系统的方式api和rpc,因此, +以上系统按照目录结构来拆分有如下结构: + +```text +. +├── afterSale +│   ├── api +│   └── rpc +├── cart +│   ├── api +│   └── rpc +├── order +│   ├── api +│   └── rpc +├── pay +│   ├── api +│   └── rpc +├── product +│   ├── api +│   └── rpc +└── user + ├── api + └── rpc +``` + +## rpc调用链建议 +在设计系统时,尽量做到服务之间调用链是单向的,而非循环调用,例如:order服务调用了user服务,而user服务反过来也会调用order的服务, +当其中一个服务启动故障,就会相互影响,进入死循环,你order认为是user服务故障导致的,而user认为是order服务导致的,如果有大量服务存在相互调用链, +则需要考虑服务拆分是否合理。 + +## 常见服务类型的目录结构 +在上述服务中,仅列举了api/rpc服务,除此之外,一个服务下还可能有其他更多服务类型,如rmq(消息处理系统),cron(定时任务系统),script(脚本)等, +因此一个服务下可能包含以下目录结构: +```text +user + ├── api // http访问服务,业务需求实现 + ├── cronjob // 定时任务,定时数据更新业务 + ├── rmq // 消息处理系统:mq和dq,处理一些高并发和延时消息业务 + ├── rpc // rpc服务,给其他子系统提供基础数据访问 + └── script // 脚本,处理一些临时运营需求,临时数据修复 +``` + +## 完整工程目录结构示例 +```text +mall // 工程名称 +├── common // 通用库 +│   ├── randx +│   └── stringx +├── go.mod +├── go.sum +└── service // 服务存放目录 + ├── afterSale + │   ├── api + │   └── model + │   └── rpc + ├── cart + │   ├── api + │   └── model + │   └── rpc + ├── order + │   ├── api + │   └── model + │   └── rpc + ├── pay + │   ├── api + │   └── model + │   └── rpc + ├── product + │   ├── api + │   └── model + │   └── rpc + └── user + ├── api + ├── cronjob + ├── model + ├── rmq + ├── rpc + └── script +``` + +# 猜你想看 +* [api目录结构介绍](api-dir.md) diff --git a/go-zero.dev/cn/service-monitor.md b/go-zero.dev/cn/service-monitor.md new file mode 100644 index 00000000..45da4f1c --- /dev/null +++ b/go-zero.dev/cn/service-monitor.md @@ -0,0 +1,100 @@ +# 服务监控 +在微服务治理中,服务监控也是非常重要的一个环节,监控一个服务是否正常工作,需要从多维度进行,如: +* mysql指标 +* mongo指标 +* redis指标 +* 请求日志 +* 服务指标统计 +* 服务健康检测 +... + +监控的工作非常大,本节仅以其中的`服务指标监控`作为例子进行说明。 + +## 基于prometheus的微服务指标监控 + +服务上线后我们往往需要对服务进行监控,以便能及早发现问题并做针对性的优化,监控又可分为多种形式,比如日志监控,调用链监控,指标监控等等。而通过指标监控能清晰的观察出服务指标的变化趋势,了解服务的运行状态,对于保证服务稳定起着非常重要的作用 +prometheus是一个开源的系统监控和告警工具,支持强大的查询语言PromQL允许用户实时选择和汇聚时间序列数据,时间序列数据是服务端通过HTTP协议主动拉取获得,也可以通过中间网关来推送时间序列数据,可以通过静态配置文件或服务发现来获取监控目标 + +##Prometheus 的架构 + +Prometheus 的整体架构以及生态系统组件如下图所示: +![prometheus-flow](./resource/prometheus-flow.png) + +Prometheus Server直接从监控目标中或者间接通过推送网关来拉取监控指标,它在本地存储所有抓取到样本数据,并对此数据执行一系列规则,以汇总和记录现有数据的新时间序列或生成告警。可以通过 Grafana 或者其他工具来实现监控数据的可视化 + +## go-zero基于prometheus的服务指标监控 + +go-zero 框架中集成了基于prometheus的服务指标监控,下面我们通过go-zero官方的示例shorturl来演示是如何对服务指标进行收集监控的: +* 第一步需要先安装Prometheus,安装步骤请参考官方文档 +* go-zero默认不开启prometheus监控,开启方式很简单,只需要在shorturl-api.yaml文件中增加配置如下,其中Host为Prometheus Server地址为必填配置,Port端口不填默认9091,Path为用来拉取指标的路径默认为/metrics + ```yaml + Prometheus: + Host: 127.0.0.1 + Port: 9091 + Path: /metrics + ``` + +* 编辑prometheus的配置文件prometheus.yml,添加如下配置,并创建targets.json + ```yaml + - job_name: 'file_ds' + file_sd_configs: + - files: + - targets.json + ``` +* 编辑targets.json文件,其中targets为shorturl配置的目标地址,并添加了几个默认的标签 + ```yaml + [ + { + "targets": ["127.0.0.1:9091"], + "labels": { + "job": "shorturl-api", + "app": "shorturl-api", + "env": "test", + "instance": "127.0.0.1:8888" + } + } + ] + ``` +* 启动prometheus服务,默认侦听在9090端口 + ```shell + $ prometheus --config.file=prometheus.yml + ``` +* 在浏览器输入`http://127.0.0.1:9090/`,然后点击`Status` -> `Targets`即可看到状态为Up的Job,并且Lables栏可以看到我们配置的默认的标签 +![prometheus-start](./resource/prometheus-start.png) +通过以上几个步骤我们完成了prometheus对shorturl服务的指标监控收集的配置工作,为了演示简单我们进行了手动的配置,在实际的生产环境中一般采用定时更新配置文件或者服务发现的方式来配置监控目标,篇幅有限这里不展开讲解,感兴趣的同学请自行查看相关文档 + +## go-zero监控的指标类型 + +go-zero目前在http的中间件和rpc的拦截器中添加了对请求指标的监控。 + +主要从请求耗时和请求错误两个维度,请求耗时采用了Histogram指标类型定义了多个Buckets方便进行分位统计,请求错误采用了Counter类型,并在http metric中添加了path标签rpc metric中添加了method标签以便进行细分监控。 +接下来演示如何查看监控指标: +首先在命令行多次执行如下命令 +```shell +$ curl -i "http://localhost:8888/shorten?url=http://www.xiaoheiban.cn" +``` +打开Prometheus切换到Graph界面,在输入框中输入{path="/shorten"}指令,即可查看监控指标,如下图 +![prometheus-graph](./resource/prometheus-graph.webp) + +我们通过PromQL语法查询过滤path为/shorten的指标,结果中显示了指标名以及指标数值,其中http_server_requests_code_total指标中code值为http的状态码,200表明请求成功,http_server_requests_duration_ms_bucket中对不同bucket结果分别进行了统计,还可以看到所有的指标中都添加了我们配置的默认指标 +Console界面主要展示了查询的指标结果,Graph界面为我们提供了简单的图形化的展示界面,在实际的生产环境中我们一般使用Grafana做图形化的展示 + +## grafana可视化界面 + +grafana是一款可视化工具,功能强大,支持多种数据来源Prometheus、Elasticsearch、Graphite等,安装比较简单请参考官方文档,grafana默认端口3000,安装好后再浏览器输入http://localhost:3000/,默认账号和密码都为admin +下面演示如何基于以上指标进行可视化界面的绘制: +点击左侧边栏`Configuration`->`Data Source`->`Add data source`进行数据源添加,其中HTTP的URL为数据源的地址 +![grafana](./resource/grafana.png) + +点击左侧边栏添加dashboard,然后添加Variables方便针对不同的标签进行过滤筛选比如添加app变量用来过滤不同的服务 +![grafana-app](./resource/grafana-app.png) + +进入dashboard点击右上角Add panel添加面板,以path维度统计接口的qps +![grafana-app](./resource/grafana-qps.png) + +最终的效果如下所示,可以通过服务名称过滤不同的服务,面板展示了path为/shorten的qps变化趋势 +![grafana-app](./resource/grafana-panel.png) + +# 总结 + +以上演示了go-zero中基于prometheus+grafana服务指标监控的简单流程,生产环境中可以根据实际的场景做不同维度的监控分析。现在go-zero的监控指标主要还是针对http和rpc,这对于服务的整体监控显然还是不足的,比如容器资源的监控,依赖的mysql、redis等资源的监控,以及自定义的指标监控等等,go-zero在这方面后续还会持续优化。希望这篇文章能够给您带来帮助 \ No newline at end of file diff --git a/doc/sharedcalls.md b/go-zero.dev/cn/sharedcalls.md similarity index 95% rename from doc/sharedcalls.md rename to go-zero.dev/cn/sharedcalls.md index d640a28b..991e726e 100644 --- a/doc/sharedcalls.md +++ b/go-zero.dev/cn/sharedcalls.md @@ -2,13 +2,13 @@ go-zero微服务框架中提供了许多开箱即用的工具,好的工具不仅能提升服务的性能而且还能提升代码的鲁棒性避免出错,实现代码风格的统一方便他人阅读等等。 -本文主要讲述进程内共享调用神器[SharedCalls](https://github.com/tal-tech/go-zero/blob/master/core/syncx/sharedcalls.go)。 +本文主要讲述进程内共享调用神器[SharedCalls](https://github.com/zeromicro/go-zero/blob/master/core/syncx/sharedcalls.go)。 ## 使用场景 并发场景下,可能会有多个线程(协程)同时请求同一份资源,如果每个请求都要走一遍资源的请求过程,除了比较低效之外,还会对资源服务造成并发的压力。举一个具体例子,比如缓存失效,多个请求同时到达某服务请求某资源,该资源在缓存中已经失效,此时这些请求会继续访问DB做查询,会引起数据库压力瞬间增大。而使用SharedCalls可以使得同时多个请求只需要发起一次拿结果的调用,其他请求"坐享其成",这种设计有效减少了资源服务的并发压力,可以有效防止缓存击穿。 -高并发场景下,当某个热点key缓存失效后,多个请求会同时从数据库加载该资源,并保存到缓存,如果不做防范,可能会导致数据库被直接打死。针对这种场景,go-zero框架中已经提供了实现,具体可参看[sqlc](https://github.com/tal-tech/go-zero/blob/master/core/stores/sqlc/cachedsql.go)和[mongoc](https://github.com/tal-tech/go-zero/blob/master/core/stores/mongoc/cachedcollection.go)等实现代码。 +高并发场景下,当某个热点key缓存失效后,多个请求会同时从数据库加载该资源,并保存到缓存,如果不做防范,可能会导致数据库被直接打死。针对这种场景,go-zero框架中已经提供了实现,具体可参看[sqlc](https://github.com/zeromicro/go-zero/blob/master/core/stores/sqlc/cachedsql.go)和[mongoc](https://github.com/zeromicro/go-zero/blob/master/core/stores/mongoc/cachedcollection.go)等实现代码。 为了简化演示代码,我们通过多个线程同时去获取一个id来模拟缓存的场景。如下: diff --git a/go-zero.dev/cn/shorturl.md b/go-zero.dev/cn/shorturl.md new file mode 100644 index 00000000..72ec58eb --- /dev/null +++ b/go-zero.dev/cn/shorturl.md @@ -0,0 +1,588 @@ +# 快速构建高并发微服务 + +[English](../en/shorturl-en.md) | 简体中文 + +## 0. 为什么说做好微服务很难 + +要想做好微服务,我们需要理解和掌握的知识点非常多,从几个维度上来说: + +* 基本功能层面 + 1. 并发控制 & 限流,避免服务被突发流量击垮 + 2. 服务注册与服务发现,确保能够动态侦测增减的节点 + 3. 负载均衡,需要根据节点承受能力分发流量 + 4. 超时控制,避免对已超时请求做无用功 + 5. 熔断设计,快速失败,保障故障节点的恢复能力 + +* 高阶功能层面 + 1. 请求认证,确保每个用户只能访问自己的数据 + 2. 链路追踪,用于理解整个系统和快速定位特定请求的问题 + 3. 日志,用于数据收集和问题定位 + 4. 可观测性,没有度量就没有优化 + +对于其中每一点,我们都需要用很长的篇幅来讲述其原理和实现,那么对我们后端开发者来说,要想把这些知识点都掌握并落实到业务系统里,难度是非常大的,不过我们可以依赖已经被大流量验证过的框架体系。[go-zero 微服务框架](https://github.com/zeromicro/go-zero)就是为此而生。 + +另外,我们始终秉承 **工具大于约定和文档** 的理念。我们希望尽可能减少开发人员的心智负担,把精力都投入到产生业务价值的代码上,减少重复代码的编写,所以我们开发了 `goctl` 工具。 + +下面我通过短链微服务来演示通过 [go-zero](https://github.com/zeromicro/go-zero) 快速的创建微服务的流程,走完一遍,你就会发现:原来编写微服务如此简单! + +## 1. 什么是短链服务 + +短链服务就是将长的 URL 网址,通过程序计算等方式,转换为简短的网址字符串。 + +写此短链服务是为了从整体上演示 go-zero 构建完整微服务的过程,算法和实现细节尽可能简化了,所以这不是一个高阶的短链服务。 + +## 2. 短链微服务架构图 + +架构图 + +* 这里只用了 `Transform RPC` 一个微服务,并不是说 API Gateway 只能调用一个微服务,只是为了最简演示 API Gateway 如何调用 RPC 微服务而已 +* 在真正项目里要尽可能每个微服务使用自己的数据库,数据边界要清晰 + +## 3. goctl 各层代码生成一览 + +所有绿色背景的功能模块是自动生成的,按需激活,红色模块是需要自己写的,也就是增加下依赖,编写业务特有逻辑,各层示意图分别如下: + +* API Gateway + + api + +* RPC + + 架构图 + +* model + + model + +下面我们来一起完整走一遍快速构建微服务的流程,Let’s `Go`!🏃‍♂️ + +## 4. 准备工作 + +* 安装 etcd, mysql, redis + +* 安装 `protoc-gen-go` + + ```shell + go get -u github.com/golang/protobuf/protoc-gen-go@v1.3.2 + ``` +* 安装 `protoc` + ``` shell + wget https://github.com/protocolbuffers/protobuf/releases/download/v3.14.0/protoc-3.14.0-linux-x86_64.zip + unzip protoc-3.14.0-linux-x86_64.zip + mv bin/protoc /usr/local/bin/ + ``` + +* 安装 goctl 工具 + + ```shell + GO111MODULE=on GOPROXY=https://goproxy.cn/,direct go get -u github.com/tal-tech/go-zero/tools/goctl + ``` + +* 创建工作目录 `shorturl` 和 `shorturl/api` + +`mkdir -p shorturl/api` + +* 在 `shorturl` 目录下执行 `go mod init shorturl` 初始化 `go.mod` + + ```Plain Text + module shorturl + + go 1.15 + + require ( + github.com/golang/mock v1.4.3 + github.com/golang/protobuf v1.4.2 + github.com/tal-tech/go-zero v1.1.4 + golang.org/x/net v0.0.0-20200707034311-ab3426394381 + google.golang.org/grpc v1.29.1 + ) + ``` + + **注意:这里可能存在 grpc 版本依赖的问题,可以用以上配置** + +## 5. 编写 API Gateway 代码 + +* 在 `shorturl/api` 目录下通过 goctl 生成 `api/shorturl.api`: + + ```shell + goctl api -o shorturl.api + ``` + +* 编辑 `api/shorturl.api`,为了简洁,去除了文件开头的 `info`,代码如下: + + ```go + type ( + expandReq { + shorten string `form:"shorten"` + } + + expandResp { + url string `json:"url"` + } + ) + + type ( + shortenReq { + url string `form:"url"` + } + + shortenResp { + shorten string `json:"shorten"` + } + ) + + service shorturl-api { + @server( + handler: ShortenHandler + ) + get /shorten(shortenReq) returns(shortenResp) + + @server( + handler: ExpandHandler + ) + get /expand(expandReq) returns(expandResp) + } + ``` + + type 用法和 go 一致,service 用来定义 get/post/head/delete 等 api 请求,解释如下: + + * `service shorturl-api {` 这一行定义了 service 名字 + * `@server` 部分用来定义 server 端用到的属性 + * `handler` 定义了服务端 handler 名字 + * `get /shorten(shortenReq) returns(shortenResp)` 定义了 get 方法的路由、请求参数、返回参数等 + +* 使用 goctl 生成 API Gateway 代码 + + ```shell + goctl api go -api shorturl.api -dir . + ``` + + 生成的文件结构如下: + + ```Plain Text + . + ├── api + │ ├── etc + │ │ └── shorturl-api.yaml // 配置文件 + │ ├── internal + │ │ ├── config + │ │ │ └── config.go // 定义配置 + │ │ ├── handler + │ │ │ ├── expandhandler.go // 实现 expandHandler + │ │ │ ├── routes.go // 定义路由处理 + │ │ │ └── shortenhandler.go // 实现 shortenHandler + │ │ ├── logic + │ │ │ ├── expandlogic.go // 实现 ExpandLogic + │ │ │ └── shortenlogic.go // 实现 ShortenLogic + │ │ ├── svc + │ │ │ └── servicecontext.go // 定义 ServiceContext + │ │ └── types + │ │ └── types.go // 定义请求、返回结构体 + │ ├── shorturl.api + │ └── shorturl.go // main 入口定义 + ├── go.mod + └── go.sum + ``` + +* 启动 API Gateway 服务,默认侦听在 8888 端口 + + ```shell + go run shorturl.go -f etc/shorturl-api.yaml + ``` + +* 测试 API Gateway 服务 + + ```shell + curl -i "http://localhost:8888/shorten?url=http://www.xiaoheiban.cn" + ``` + + 返回如下: + + ```http + HTTP/1.1 200 OK + Content-Type: application/json + Date: Thu, 27 Aug 2020 14:31:39 GMT + Content-Length: 15 + + {"shorten":""} + ``` + + 可以看到我们 API Gateway 其实啥也没干,就返回了个空值,接下来我们会在 rpc 服务里实现业务逻辑 + +* 可以修改 `internal/svc/servicecontext.go` 来传递服务依赖(如果需要) + +* 实现逻辑可以修改 `internal/logic` 下的对应文件 + +* 可以通过 `goctl` 生成各种客户端语言的 api 调用代码 + +* 到这里,你已经可以通过 goctl 生成客户端代码给客户端同学并行开发了,支持多种语言,详见文档 + +## 6. 编写 transform rpc 服务 + +- 在 `shorturl` 目录下创建 `rpc` 目录 + +* 在 `rpc/transform` 目录下编写 `transform.proto` 文件 + + 可以通过命令生成 proto 文件模板 + + ```shell + goctl rpc template -o transform.proto + ``` + + 修改后文件内容如下: + + ```protobuf + syntax = "proto3"; + + package transform; + + message expandReq { + string shorten = 1; + } + + message expandResp { + string url = 1; + } + + message shortenReq { + string url = 1; + } + + message shortenResp { + string shorten = 1; + } + + service transformer { + rpc expand(expandReq) returns(expandResp); + rpc shorten(shortenReq) returns(shortenResp); + } + ``` + +* 用 `goctl` 生成 rpc 代码,在 `rpc/transform` 目录下执行命令 + + ```shell + goctl rpc proto -src transform.proto -dir . + ``` + + **注意:不能在 GOPATH 目录下执行以上命令** + + 文件结构如下: + + ```Plain Text + rpc/transform + ├── etc + │ └── transform.yaml // 配置文件 + ├── internal + │ ├── config + │ │ └── config.go // 配置定义 + │ ├── logic + │ │ ├── expandlogic.go // expand 业务逻辑在这里实现 + │ │ └── shortenlogic.go // shorten 业务逻辑在这里实现 + │ ├── server + │ │ └── transformerserver.go // 调用入口, 不需要修改 + │ └── svc + │ └── servicecontext.go // 定义 ServiceContext,传递依赖 + ├── pb + │ └── transform.pb.go + ├── transform.go // rpc 服务 main 函数 + ├── transform.proto + └── transformer + ├── transformer.go // 提供了外部调用方法,无需修改 + ├── transformer_mock.go // mock 方法,测试用 + └── types.go // request/response 结构体定义 + ``` + + 直接可以运行,如下: + + ```shell + $ go run transform.go -f etc/transform.yaml + Starting rpc server at 127.0.0.1:8080... + ``` + 查看服务是否注册 + ``` + $ETCDCTL_API=3 etcdctl get transform.rpc --prefix + transform.rpc/7587851893787585061 + 127.0.0.1:8080 + ``` + `etc/transform.yaml` 文件里可以修改侦听端口等配置 + +## 7. 修改 API Gateway 代码调用 transform rpc 服务 + +* 修改配置文件 `shorturl-api.yaml`,增加如下内容 + + ```yaml + Transform: + Etcd: + Hosts: + - localhost:2379 + Key: transform.rpc + ``` + + 通过 etcd 自动去发现可用的 transform 服务 + +* 修改 `internal/config/config.go` 如下,增加 transform 服务依赖 + + ```go + type Config struct { + rest.RestConf + Transform zrpc.RpcClientConf // 手动代码 + } + ``` + +* 修改 `internal/svc/servicecontext.go`,如下: + + ```go + type ServiceContext struct { + Config config.Config + Transformer transformer.Transformer // 手动代码 + } + + func NewServiceContext(c config.Config) *ServiceContext { + return &ServiceContext{ + Config: c, + Transformer: transformer.NewTransformer(zrpc.MustNewClient(c.Transform)), // 手动代码 + } + } + ``` + + 通过 ServiceContext 在不同业务逻辑之间传递依赖 + +* 修改 `internal/logic/expandlogic.go` 里的 `Expand` 方法,如下: + + ```go + func (l *ExpandLogic) Expand(req types.ExpandReq) (types.ExpandResp, error) { + // 手动代码开始 + resp, err := l.svcCtx.Transformer.Expand(l.ctx, &transformer.ExpandReq{ + Shorten: req.Shorten, + }) + if err != nil { + return types.ExpandResp{}, err + } + + return types.ExpandResp{ + Url: resp.Url, + }, nil + // 手动代码结束 + } + ``` + +通过调用 `transformer` 的 `Expand` 方法实现短链恢复到 url + +* 修改 `internal/logic/shortenlogic.go`,如下: + + ```go + func (l *ShortenLogic) Shorten(req types.ShortenReq) (types.ShortenResp, error) { + // 手动代码开始 + resp, err := l.svcCtx.Transformer.Shorten(l.ctx, &transformer.ShortenReq{ + Url: req.Url, + }) + if err != nil { + return types.ShortenResp{}, err + } + + return types.ShortenResp{ + Shorten: resp.Shorten, + }, nil + // 手动代码结束 + } + ``` +有的版本生成返回值可能是指针类型,需要自己调整下 + +通过调用 `transformer` 的 `Shorten` 方法实现 url 到短链的变换 + +至此,API Gateway 修改完成,虽然贴的代码多,但是其中修改的是很少的一部分,为了方便理解上下文,我贴了完整代码,接下来处理 CRUD+cache + +## 8. 定义数据库表结构,并生成 CRUD+cache 代码 + +* shorturl 下创建 `rpc/transform/model` 目录:`mkdir -p rpc/transform/model` + +* 在 `rpc/transform/model` 目录下编写创建 shorturl 表的 sql 文件 `shorturl.sql`,如下: + + ```sql + CREATE TABLE `shorturl` + ( + `shorten` varchar(255) NOT NULL COMMENT 'shorten key', + `url` varchar(255) NOT NULL COMMENT 'original url', + PRIMARY KEY(`shorten`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + ``` + +* 创建 DB 和 table + + ```sql + create database gozero; + ``` + + ```sql + source shorturl.sql; + ``` + +* 在 `rpc/transform/model` 目录下执行如下命令生成 CRUD+cache 代码,`-c` 表示使用 `redis cache` + + ```shell + goctl model mysql ddl -c -src shorturl.sql -dir . + ``` + + 也可以用 `datasource` 命令代替 `ddl` 来指定数据库链接直接从 schema 生成 + + 生成后的文件结构如下: + + ```Plain Text + rpc/transform/model + ├── shorturl.sql + ├── shorturlmodel.go // CRUD+cache 代码 + └── vars.go // 定义常量和变量 + ``` + +## 9. 修改 shorten/expand rpc 代码调用 crud+cache 代码 + +* 修改 `rpc/transform/etc/transform.yaml`,增加如下内容: + + ```yaml + DataSource: root:password@tcp(localhost:3306)/gozero + Table: shorturl + Cache: + - Host: localhost:6379 + ``` + + 可以使用多个 redis 作为 cache,支持 redis 单点或者 redis 集群 + +* 修改 `rpc/transform/internal/config/config.go`,如下: + + ```go + type Config struct { + zrpc.RpcServerConf + DataSource string // 手动代码 + Table string // 手动代码 + Cache cache.CacheConf // 手动代码 + } + ``` + + 增加了 mysql 和 redis cache 配置 + +* 修改 `rpc/transform/internal/svc/servicecontext.go`,如下: + + ```go + type ServiceContext struct { + c config.Config + Model model.ShorturlModel // 手动代码 + } + + func NewServiceContext(c config.Config) *ServiceContext { + return &ServiceContext{ + c: c, + Model: model.NewShorturlModel(sqlx.NewMysql(c.DataSource), c.Cache), // 手动代码 + } + } + ``` + +* 修改 `rpc/transform/internal/logic/expandlogic.go`,如下: + + ```go + func (l *ExpandLogic) Expand(in *transform.ExpandReq) (*transform.ExpandResp, error) { + // 手动代码开始 + res, err := l.svcCtx.Model.FindOne(in.Shorten) + if err != nil { + return nil, err + } + + return &transform.ExpandResp{ + Url: res.Url, + }, nil + // 手动代码结束 + } + ``` + +* 修改 `rpc/transform/internal/logic/shortenlogic.go`,如下: + + ```go + func (l *ShortenLogic) Shorten(in *transform.ShortenReq) (*transform.ShortenResp, error) { + // 手动代码开始,生成短链接 + key := hash.Md5Hex([]byte(in.Url))[:6] + _, err := l.svcCtx.Model.Insert(model.Shorturl{ + Shorten: key, + Url: in.Url, + }) + if err != nil { + return nil, err + } + + return &transform.ShortenResp{ + Shorten: key, + }, nil + // 手动代码结束 + } + ``` + + 至此代码修改完成,凡是手动修改的代码我加了标注 + + **注意:** + 1. undefined cache,你需要 `import "github.com/tal-tech/go-zero/core/stores/cache"` + 2. undefined model, sqlx, hash 等,你需要在文件中 + + ```golang + import "shorturl/rpc/transform/model" + + import "github.com/tal-tech/go-zero/core/stores/sqlx" + ``` + +## 10. 完整调用演示 + +* shorten api 调用 + + ```shell + curl -i "http://localhost:8888/shorten?url=http://www.xiaoheiban.cn" + ``` + + 返回如下: + + ```http + HTTP/1.1 200 OK + Content-Type: application/json + Date: Sat, 29 Aug 2020 10:49:49 GMT + Content-Length: 21 + + {"shorten":"f35b2a"} + ``` + +* expand api 调用 + + ```shell + curl -i "http://localhost:8888/expand?shorten=f35b2a" + ``` + + 返回如下: + + ```http + HTTP/1.1 200 OK + Content-Type: application/json + Date: Sat, 29 Aug 2020 10:51:53 GMT + Content-Length: 34 + + {"url":"http://www.xiaoheiban.cn"} + ``` + +## 11. Benchmark + +因为写入依赖于 mysql 的写入速度,就相当于压 mysql 了,所以压测只测试了 expand 接口,相当于从 mysql 里读取并利用缓存,shorten.lua 里随机从 db 里获取了 100 个热 key 来生成压测请求 + +![Benchmark](https://gitee.com/kevwan/static/raw/master/doc/images/shorturl-benchmark.png) + +可以看出在我的 MacBook Pro 上能达到 3 万 + 的 qps。 + +## 12. 完整代码 + +[https://github.com/zeromicro/zero-examples/tree/main/shorturl](https://github.com/zeromicro/zero-examples/tree/main/shorturl) + +## 12. 总结 + +我们一直强调 **工具大于约定和文档**。 + +go-zero 不只是一个框架,更是一个建立在框架 + 工具基础上的,简化和规范了整个微服务构建的技术体系。 + +我们在保持简单的同时也尽可能把微服务治理的复杂度封装到了框架内部,极大的降低了开发人员的心智负担,使得业务开发得以快速推进。 + +通过 go-zero+goctl 生成的代码,包含了微服务治理的各种组件,包括:并发控制、自适应熔断、自适应降载、自动缓存控制等,可以轻松部署以承载巨大访问量。 + +有任何好的提升工程效率的想法,随时欢迎交流!👏 + diff --git a/go-zero.dev/cn/source.md b/go-zero.dev/cn/source.md new file mode 100644 index 00000000..881e94d4 --- /dev/null +++ b/go-zero.dev/cn/source.md @@ -0,0 +1,2 @@ +# 相关源码 +* [demo源码](https://github.com/zeromicro/go-zero-demo) \ No newline at end of file diff --git a/go-zero.dev/cn/sql-cache.md b/go-zero.dev/cn/sql-cache.md new file mode 100644 index 00000000..508e8b6e --- /dev/null +++ b/go-zero.dev/cn/sql-cache.md @@ -0,0 +1,23 @@ +# DB缓存机制 + +## QueryRowIndex + +* 没有查询条件到Primary映射的缓存 + * 通过查询条件到DB去查询行记录,然后 + * **把Primary到行记录的缓存写到redis里** + * **把查询条件到Primary的映射保存到redis里**,*框架的Take方法自动做了* + * 可能的过期顺序 + * 查询条件到Primary的映射缓存未过期 + * Primary到行记录的缓存未过期 + * 直接返回缓存行记录 + * Primary到行记录的缓存已过期 + * 通过Primary到DB获取行记录,并写入缓存 + * 此时存在的问题是,查询条件到Primary的缓存可能已经快要过期了,短时间内的查询又会触发一次数据库查询 + * 要避免这个问题,可以让**上面粗体部分**第一个过期时间略长于第二个,比如5秒 + * 查询条件到Primary的映射缓存已过期,不管Primary到行记录的缓存是否过期 + * 查询条件到Primary的映射会被重新获取,获取过程中会自动写入新的Primary到行记录的缓存,这样两种缓存的过期时间都是刚刚设置 +* 有查询条件到Primary映射的缓存 + * 没有Primary到行记录的缓存 + * 通过Primary到DB查询行记录,并写入缓存 + * 有Primary到行记录的缓存 + * 直接返回缓存结果 diff --git a/go-zero.dev/cn/stream.md b/go-zero.dev/cn/stream.md new file mode 100644 index 00000000..33ce302b --- /dev/null +++ b/go-zero.dev/cn/stream.md @@ -0,0 +1,366 @@ +# 流数据处理利器 + +流处理 (Stream processing) 是一种计算机编程范式,其允许给定一个数据序列 (流处理数据源),一系列数据操作 (函数) 被应用到流中的每个元素。同时流处理工具可以显著提高程序员的开发效率,允许他们编写有效、干净和简洁的代码。 + +流数据处理在我们的日常工作中非常常见,举个例子,我们在业务开发中往往会记录许多业务日志,这些日志一般是先发送到 Kafka,然后再由 Job 消费 Kafaka 写到 elasticsearch,在进行日志流处理的过程中,往往还会对日志做一些处理,比如过滤无效的日志,做一些计算以及重新组合日志等等,示意图如下: +![fx_log.png](./resource/fx_log.png) +### 流处理工具fx +[go-zero](https://github.com/zeromicro/go-zero) 是一个功能完备的微服务框架,框架中内置了很多非常实用的工具,其中就包含流数据处理工具[fx](https://github.com/zeromicro/go-zero/tree/master/core/fx) ,下面我们通过一个简单的例子来认识下该工具: +```go +package main + +import ( + "fmt" + "os" + "os/signal" + "syscall" + "time" + + "github.com/tal-tech/go-zero/core/fx" +) + +func main() { + ch := make(chan int) + + go inputStream(ch) + go outputStream(ch) + + c := make(chan os.Signal, 1) + signal.Notify(c, syscall.SIGTERM, syscall.SIGINT) + <-c +} + +func inputStream(ch chan int) { + count := 0 + for { + ch <- count + time.Sleep(time.Millisecond * 500) + count++ + } +} + +func outputStream(ch chan int) { + fx.From(func(source chan<- interface{}) { + for c := range ch { + source <- c + } + }).Walk(func(item interface{}, pipe chan<- interface{}) { + count := item.(int) + pipe <- count + }).Filter(func(item interface{}) bool { + itemInt := item.(int) + if itemInt%2 == 0 { + return true + } + return false + }).ForEach(func(item interface{}) { + fmt.Println(item) + }) +} +``` + + +inputStream函数模拟了流数据的产生,outputStream函数模拟了流数据的处理过程,其中From函数为流的输入,Walk函数并发的作用在每一个item上,Filter函数对item进行过滤为true保留为false不保留,ForEach函数遍历输出每一个item元素。 + + +### 流数据处理中间操作 + + +一个流的数据处理可能存在许多的中间操作,每个中间操作都可以作用在流上。就像流水线上的工人一样,每个工人操作完零件后都会返回处理完成的新零件,同理流处理中间操作完成后也会返回一个新的流。 +![7715f4b6-8739-41ac-8c8c-04d187172e9d.png](./resource/7715f4b6-8739-41ac-8c8c-04d187172e9d.png) +fx的流处理中间操作: + +| 操作函数 | 功能 | 输入 | +| --- | --- | --- | +| Distinct | 去除重复的item | KeyFunc,返回需要去重的key | +| Filter | 过滤不满足条件的item | FilterFunc,Option控制并发量 | +| Group | 对item进行分组 | KeyFunc,以key进行分组 | +| Head | 取出前n个item,返回新stream | int64保留数量 | +| Map | 对象转换 | MapFunc,Option控制并发量 | +| Merge | 合并item到slice并生成新stream | | +| Reverse | 反转item | | +| Sort | 对item进行排序 | LessFunc实现排序算法 | +| Tail | 与Head功能类似,取出后n个item组成新stream | int64保留数量 | +| Walk | 作用在每个item上 | WalkFunc,Option控制并发量 | + + + +下图展示了每个步骤和每个步骤的结果: + + +![3aefec98-56eb-45a6-a4b2-9adbdf4d63c0.png](./resource/3aefec98-56eb-45a6-a4b2-9adbdf4d63c0.png) + + +### 用法与原理分析 + + +#### From + + +通过From函数构建流并返回Stream,流数据通过channel进行存储: + + +```go +// 例子 +s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0} +fx.From(func(source chan<- interface{}) { + for _, v := range s { + source <- v + } +}) + +// 源码 +func From(generate GenerateFunc) Stream { + source := make(chan interface{}) + + threading.GoSafe(func() { + defer close(source) + // 构造流数据写入channel + generate(source) + }) + + return Range(source) +} +``` + + +#### Filter + + +Filter函数提供过滤item的功能,FilterFunc定义过滤逻辑true保留item,false则不保留: + + +```go +// 例子 保留偶数 +s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0} +fx.From(func(source chan<- interface{}) { + for _, v := range s { + source <- v + } +}).Filter(func(item interface{}) bool { + if item.(int)%2 == 0 { + return true + } + return false +}) + +// 源码 +func (p Stream) Filter(fn FilterFunc, opts ...Option) Stream { + return p.Walk(func(item interface{}, pipe chan<- interface{}) { + // 执行过滤函数true保留,false丢弃 + if fn(item) { + pipe <- item + } + }, opts...) +} +``` + + +#### Group + + +Group对流数据进行分组,需定义分组的key,数据分组后以slice存入channel: + + +```go +// 例子 按照首字符"g"或者"p"分组,没有则分到另一组 + ss := []string{"golang", "google", "php", "python", "java", "c++"} + fx.From(func(source chan<- interface{}) { + for _, s := range ss { + source <- s + } + }).Group(func(item interface{}) interface{} { + if strings.HasPrefix(item.(string), "g") { + return "g" + } else if strings.HasPrefix(item.(string), "p") { + return "p" + } + return "" + }).ForEach(func(item interface{}) { + fmt.Println(item) + }) +} + +// 源码 +func (p Stream) Group(fn KeyFunc) Stream { + // 定义分组存储map + groups := make(map[interface{}][]interface{}) + for item := range p.source { + // 用户自定义分组key + key := fn(item) + // key相同分到一组 + groups[key] = append(groups[key], item) + } + + source := make(chan interface{}) + go func() { + for _, group := range groups { + // 相同key的一组数据写入到channel + source <- group + } + close(source) + }() + + return Range(source) +} +``` + + +#### Reverse + + +reverse可以对流中元素进行反转处理: + + +![7e0fd2b8-d4c1-4130-a216-a7d3d4301116.png](./resource/7e0fd2b8-d4c1-4130-a216-a7d3d4301116.png) + + +```go +// 例子 +fx.Just(1, 2, 3, 4, 5).Reverse().ForEach(func(item interface{}) { + fmt.Println(item) +}) + +// 源码 +func (p Stream) Reverse() Stream { + var items []interface{} + // 获取流中数据 + for item := range p.source { + items = append(items, item) + } + // 反转算法 + for i := len(items)/2 - 1; i >= 0; i-- { + opp := len(items) - 1 - i + items[i], items[opp] = items[opp], items[i] + } + + // 写入流 + return Just(items...) +} +``` + + +#### Distinct + + +distinct对流中元素进行去重,去重在业务开发中比较常用,经常需要对用户id等做去重操作: + + +```go +// 例子 +fx.Just(1, 2, 2, 2, 3, 3, 4, 5, 6).Distinct(func(item interface{}) interface{} { + return item +}).ForEach(func(item interface{}) { + fmt.Println(item) +}) +// 结果为 1,2,3,4,5,6 + +// 源码 +func (p Stream) Distinct(fn KeyFunc) Stream { + source := make(chan interface{}) + + threading.GoSafe(func() { + defer close(source) + // 通过key进行去重,相同key只保留一个 + keys := make(map[interface{}]lang.PlaceholderType) + for item := range p.source { + key := fn(item) + // key存在则不保留 + if _, ok := keys[key]; !ok { + source <- item + keys[key] = lang.Placeholder + } + } + }) + + return Range(source) +} +``` + + +#### Walk + + +Walk函数并发的作用在流中每一个item上,可以通过WithWorkers设置并发数,默认并发数为16,最小并发数为1,如设置unlimitedWorkers为true则并发数无限制,但并发写入流中的数据由defaultWorkers限制,WalkFunc中用户可以自定义后续写入流中的元素,可以不写入也可以写入多个元素: + + +```go +// 例子 +fx.Just("aaa", "bbb", "ccc").Walk(func(item interface{}, pipe chan<- interface{}) { + newItem := strings.ToUpper(item.(string)) + pipe <- newItem +}).ForEach(func(item interface{}) { + fmt.Println(item) +}) + +// 源码 +func (p Stream) walkLimited(fn WalkFunc, option *rxOptions) Stream { + pipe := make(chan interface{}, option.workers) + + go func() { + var wg sync.WaitGroup + pool := make(chan lang.PlaceholderType, option.workers) + + for { + // 控制并发数量 + pool <- lang.Placeholder + item, ok := <-p.source + if !ok { + <-pool + break + } + + wg.Add(1) + go func() { + defer func() { + wg.Done() + <-pool + }() + // 作用在每个元素上 + fn(item, pipe) + }() + } + + // 等待处理完成 + wg.Wait() + close(pipe) + }() + + return Range(pipe) +} +``` + + +### 并发处理 + + +fx工具除了进行流数据处理以外还提供了函数并发功能,在微服务中实现某个功能往往需要依赖多个服务,并发的处理依赖可以有效的降低依赖耗时,提升服务的性能。 + + +![b97bf7df-1781-436e-bf04-f1dd90c60537.png](./resource/b97bf7df-1781-436e-bf04-f1dd90c60537.png) + + +```go +fx.Parallel(func() { + userRPC() // 依赖1 +}, func() { + accountRPC() // 依赖2 +}, func() { + orderRPC() // 依赖3 +}) +``` + + +注意fx.Parallel进行依赖并行处理的时候不会有error返回,如需有error返回或者有一个依赖报错需要立马结束依赖请求请使用[MapReduce](https://gocn.vip/topics/10941) 工具进行处理。 + + +### 总结 + + +本篇文章介绍了流处理的基本概念和go-zero中的流处理工具fx,在实际的生产中流处理场景应用也非常多,希望本篇文章能给大家带来一定的启发,更好的应对工作中的流处理场景。 + + + + + + diff --git a/go-zero.dev/cn/summary.md b/go-zero.dev/cn/summary.md new file mode 100644 index 00000000..b9cd106f --- /dev/null +++ b/go-zero.dev/cn/summary.md @@ -0,0 +1,79 @@ +# Summary + +* [简介](README.md) +* [阅读须知](tips.md) +* [关于我们](about-us.md) +* [加入我们](join-us.md) +* [概念介绍](concept-introduction.md) +* [快速开发](quick-start.md) + * [单体服务](monolithic-service.md) + * [微服务](micro-service.md) +* [框架设计](framework-design.md) + * [go-zero设计理念](go-zero-design.md) + * [go-zero特点](go-zero-features.md) + * [api语法介绍](api-grammar.md) + * [api目录结构](api-dir.md) + * [rpc目录结构](rpc-dir.md) +* [项目开发](project-dev.md) + * [准备工作](prepare.md) + * [golang安装](golang-install.md) + * [go module配置](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) +* [Goctl](goctl.md) + * [命令大全](goctl-commands.md) + * [api命令](goctl-api.md) + * [rpc命令](goctl-rpc.md) + * [model命令](goctl-model.md) + * [plugin命令](goctl-plugin.md) + * [其他命令](goctl-other.md) +* [模板管理](template-manage.md) + * [模板操作](template-cmd.md) + * [自定义模板](template.md) +* [扩展阅读](extended-reading.md) + * [logx](logx.md) + * [bloom](bloom.md) + * [executors](executors.md) + * [fx](fx.md) + * [mysql](mysql.md) + * [redis-lock](redis-lock.md) + * [periodlimit](periodlimit.md) + * [tokenlimit](tokenlimit.md) + * [TimingWheel](timing-wheel.md) +* [工具中心](tool-center.md) + * [intellij插件](intellij.md) + * [vscode插件](vscode.md) +* [插件中心](plugin-center.md) +* [学习资源](learning-resource.md) + * [公众号](wechat.md) + * [Go夜读](goreading.md) + * [Go开源说](gotalk.md) +* [贡献人员](contributor.md) +* [文档贡献](doc-contibute.md) +* [常见错误处理](error.md) +* [FAQ](faq.md) +* [相关源码](source.md) + diff --git a/go-zero.dev/cn/template-cmd.md b/go-zero.dev/cn/template-cmd.md new file mode 100644 index 00000000..7e56e4bb --- /dev/null +++ b/go-zero.dev/cn/template-cmd.md @@ -0,0 +1,113 @@ +# 模板操作 + +模板(Template)是数据驱动生成的基础,所有的代码(rest api、rpc、model、docker、kube)生成都会依赖模板, +默认情况下,模板生成器会选择内存中的模板进行生成,而对于有模板修改需求的开发者来讲,则需要将模板进行落盘, +从而进行模板修改,在下次代码生成时会加载指定路径下的模板进行生成。 + +## 使用帮助 +```text +NAME: + goctl template - template operation + +USAGE: + goctl template command [command options] [arguments...] + +COMMANDS: + init initialize the all templates(force update) + clean clean the all cache templates + update update template of the target category to the latest + revert revert the target template to the latest + +OPTIONS: + --help, -h show help +``` + +## 模板初始化 +```text +NAME: + goctl template init - initialize the all templates(force update) + +USAGE: + goctl template init [command options] [arguments...] + +OPTIONS: + --home value the goctl home path of the template +``` + +## 清除模板 +```text +NAME: + goctl template clean - clean the all cache templates + +USAGE: + goctl template clean [command options] [arguments...] + +OPTIONS: + --home value the goctl home path of the template +``` + +## 回滚指定分类模板 +```text +NAME: + goctl template update - update template of the target category to the latest + +USAGE: + goctl template update [command options] [arguments...] + +OPTIONS: + --category value, -c value the category of template, enum [api,rpc,model,docker,kube] + --home value the goctl home path of the template +``` + +## 回滚模板 +```text +NAME: + goctl template revert - revert the target template to the latest + +USAGE: + goctl template revert [command options] [arguments...] + +OPTIONS: + --category value, -c value the category of template, enum [api,rpc,model,docker,kube] + --name value, -n value the target file name of template + --home value the goctl home path of the template +``` + +> [!TIP] +> +> `--home` 指定模板存储路径 + +## 模板加载 + +在代码生成时可以通过`--home`来指定模板所在文件夹,目前已支持指定模板目录的命令有: + +- `goctl api go` 详情可以通过`goctl api go --help`查看帮助 +- `goctl docker` 详情可以通过`goctl docker --help`查看帮助 +- `goctl kube` 详情可以通过`goctl kube --help`查看帮助 +- `goctl rpc new` 详情可以通过`goctl rpc new --help`查看帮助 +- `goctl rpc proto` 详情可以通过`goctl rpc proto --help`查看帮助 +- `goctl model mysql ddl` 详情可以通过`goctl model mysql ddl --help`查看帮助 +- `goctl model mysql datasource` 详情可以通过`goctl model mysql datasource --help`查看帮助 +- `goctl model postgresql datasource` 详情可以通过`goctl model mysql datasource --help`查看帮助 +- `goctl model mongo` 详情可以通过`goctl model mongo --help`查看帮助 + +默认情况(在不指定`--home`)会从`$HOME/.goctl`目录下读取。 + +## 使用示例 +* 初始化模板到指定`$HOME/template`目录下 +```text +$ goctl template init --home $HOME/template +``` + +```text +Templates are generated in /Users/anqiansong/template, edit on your risk! +``` + +* 使用`$HOME/template`模板进行greet rpc生成 +```text +$ goctl rpc new greet --home $HOME/template +``` + +```text +Done +``` \ No newline at end of file diff --git a/go-zero.dev/cn/template-manage.md b/go-zero.dev/cn/template-manage.md new file mode 100644 index 00000000..3f8e6dfd --- /dev/null +++ b/go-zero.dev/cn/template-manage.md @@ -0,0 +1,4 @@ +# 模板管理 + +- [模板操作](template-cmd.md) +- [自定义模板](template-manage.md) \ No newline at end of file diff --git a/go-zero.dev/cn/template.md b/go-zero.dev/cn/template.md new file mode 100644 index 00000000..51509d84 --- /dev/null +++ b/go-zero.dev/cn/template.md @@ -0,0 +1,158 @@ +# 模板修改 + +## 场景 +实现统一格式的body响应,格式如下: +```json +{ + "code": 0, + "msg": "OK", + "data": {} // ① +} +``` + +① 实际响应数据 + +> [!TIP] +> `go-zero`生成的代码没有对其进行处理 +## 准备工作 +我们提前在`module`为`greet`的工程下的`response`包中写一个`Response`方法,目录树类似如下: +```text +greet +├── reponse +│   └── response.go +└── xxx... +``` + +代码如下 +```go +package reponse + +import ( + "net/http" + + "github.com/tal-tech/go-zero/rest/httpx" +) + +type Body struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data interface{} `json:"data,omitempty"` +} + +func Response(w http.ResponseWriter, resp interface{}, err error) { + var body Body + if err != nil { + body.Code = -1 + body.Msg = err.Error() + } else { + body.Msg = "OK" + body.Data = resp + } + httpx.OkJson(w, body) +} +``` + +## 修改`handler`模板 +```shell +$ vim ~/.goctl/api/handler.tpl +``` +将模板替换为以下内容 +```go +package handler + +import ( + "net/http" + "greet/response"// ① + + {{.ImportPackages}} +) + +func {{.HandlerName}}(ctx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + {{if .HasRequest}}var req types.{{.RequestType}} + if err := httpx.Parse(r, &req); err != nil { + httpx.Error(w, err) + return + }{{end}} + + l := logic.New{{.LogicType}}(r.Context(), ctx) + {{if .HasResp}}resp, {{end}}err := l.{{.Call}}({{if .HasRequest}}req{{end}}) + {{if .HasResp}}reponse.Response(w, resp, err){{else}}reponse.Response(w, nil, err){{end}}//② + + } +} +``` + +① 替换为你真实的`response`包名,仅供参考 + +② 自定义模板内容 + +> [!TIP] +> +> 1.如果本地没有`~/.goctl/api/handler.tpl`文件,可以通过模板初始化命令`goctl template init`进行初始化 + +## 修改模板前后对比 +* 修改前 +```go +func GreetHandler(ctx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.Request + if err := httpx.Parse(r, &req); err != nil { + httpx.Error(w, err) + return + } + + l := logic.NewGreetLogic(r.Context(), ctx) + resp, err := l.Greet(req) + // 以下内容将被自定义模板替换 + if err != nil { + httpx.Error(w, err) + } else { + httpx.OkJson(w, resp) + } + } +} +``` +* 修改后 +```go +func GreetHandler(ctx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.Request + if err := httpx.Parse(r, &req); err != nil { + httpx.Error(w, err) + return + } + + l := logic.NewGreetLogic(r.Context(), ctx) + resp, err := l.Greet(req) + reponse.Response(w, resp, err) + } +} +``` + +## 修改模板前后响应体对比 + +* 修改前 +```json +{ + "message": "Hello go-zero!" +} +``` + +* 修改后 +```json +{ + "code": 0, + "msg": "OK", + "data": { + "message": "Hello go-zero!" + } +} +``` + +# 总结 +本文档仅对http相应为例讲述了自定义模板的流程,除此之外,自定义模板的场景还有: +* model 层添加kmq +* model 层生成待有效期option的model实例 +* http自定义相应格式 + ... diff --git a/doc/timingWheel.md b/go-zero.dev/cn/timing-wheel.md similarity index 71% rename from doc/timingWheel.md rename to go-zero.dev/cn/timing-wheel.md index 3d120b29..00c4d771 100644 --- a/doc/timingWheel.md +++ b/go-zero.dev/cn/timing-wheel.md @@ -1,20 +1,22 @@ -# go-zero 如何应对海量定时/延迟任务? +# TimingWheel -一个系统中存在着大量的调度任务,同时调度任务存在时间的滞后性,而大量的调度任务如果每一个都使用自己的调度器来管理任务的生命周期的话,浪费cpu的资源而且很低效。 +本文来介绍 `go-zero` 中 **延迟操作**。**延迟操作**,可以采用两个方案: -本文来介绍 `go-zero` 中 **延迟操作**,它可能让开发者调度多个任务时,**只需关注具体的业务执行函数和执行时间「立即或者延迟」**。而 **延迟操作**,通常可以采用两个方案: 1. `Timer`:定时器维护一个优先队列,到时间点执行,然后把需要执行的 task 存储在 map 中 2. `collection` 中的 `timingWheel` ,维护一个存放任务组的数组,每一个槽都维护一个存储task的双向链表。开始执行时,计时器每隔指定时间执行一个槽里面的tasks。 + + 方案2把维护task从 `优先队列 O(nlog(n))` 降到 `双向链表 O(1)`,而执行task也只要轮询一个时间点的tasks `O(N)`,不需要像优先队列,放入和删除元素 `O(nlog(n))`。 -我们先看看 `go-zero` 中自己对 `timingWheel` 的使用 : ## cache 中的 timingWheel + 首先我们先来在 `collection` 的 `cache` 中关于 `timingWheel` 的使用: + ```go timingWheel, err := NewTimingWheel(time.Second, slots, func(k, v interface{}) { key, ok := k.(string) @@ -30,18 +32,25 @@ if err != nil { cache.timingWheel = timingWheel ``` + 这是 `cache` 初始化中也同时初始化 `timingWheel` 做key的过期处理,参数依次代表: + - `interval`:时间划分刻度 - `numSlots`:时间槽 - `execute`:时间点执行函数 + + 在 `cache` 中执行函数则是 **删除过期key**,而这个过期则由 `timingWheel` 来控制推进时间。 + **接下来,就通过 `cache` 对 `timingWheel` 的使用来认识。** + ### 初始化 + ```go // 真正做初始化 func newTimingWheelWithClock(interval time.Duration, numSlots int, execute Execute, ticker timex.Ticker) ( @@ -69,11 +78,15 @@ func newTimingWheelWithClock(interval time.Duration, numSlots int, execute Execu } ``` -![](https://gitee.com/kevwan/static/raw/master/doc/images/timewheel-struct.png) + +![76108cc071154e2faa66eada81857fb0~tplv-k3u1fbpfcp-zoom-1.image.png](./resource/76108cc071154e2faa66eada81857fb0_tplv-k3u1fbpfcp-zoom-1.image.png) + 以上比较直观展示 `timingWheel` 的 **“时间轮”**,后面会围绕这张图解释其中推进的细节。 - `go tw.run()` 开一个协程做时间推动: + +`go tw.run()` 开一个协程做时间推动: + ```go func (tw *TimingWheel) run() { @@ -91,14 +104,19 @@ func (tw *TimingWheel) run() { } ``` -可以看出,在初始化的时候就开始了 `timer` 执行,并以`internal`时间段转动,然后底层不停的获取来自 `slot` 中的 `list` 的task,交给 `execute` 执行。 -![](https://gitee.com/kevwan/static/raw/master/doc/images/timewheel-run.png) +可以看出,在初始化的时候就开始了 `timer` 执行,并以`internal`时间段转动,然后底层不停的获取来自 `slot` 中的  `list` 的task,交给 `execute` 执行。 + + +![3bbddc1ebb79455da91dfcf3da6bc72f~tplv-k3u1fbpfcp-zoom-1.image.png](./resource/3bbddc1ebb79455da91dfcf3da6bc72f_tplv-k3u1fbpfcp-zoom-1.image.png) + ### Task Operation + 紧接着就是设置 `cache key` : + ```go func (c *Cache) Set(key string, value interface{}) { c.lock.Lock() @@ -116,23 +134,30 @@ func (c *Cache) Set(key string, value interface{}) { } ``` + 1. 先看在 `data map` 中有没有存在这个key -2. 存在,则更新 `expire` -> `MoveTimer()` -3. 第一次设置key -> `SetTimer()` +2. 存在,则更新 `expire`   -> `MoveTimer()` +3. 第一次设置key   ->   `SetTimer()` + + 所以对于 `timingWheel` 的使用上就清晰了,开发者根据需求可以 `add` 或是 `update`。 + 同时我们跟源码进去会发现:`SetTimer() MoveTimer()` 都是将task输送到channel,由 `run()` 中开启的协程不断取出 `channel` 的task操作。 + > `SetTimer() -> setTask()`: -> > - not exist task:`getPostion -> pushBack to list -> setPosition` -> - exist task:`get from timers -> moveTask() ` +> - exist task:`get from timers -> moveTask()` > -> `MoveTimer() -> moveTask()` +`MoveTimer() -> moveTask()` + + 由上面的调用链,有一个都会调用的函数:`moveTask()` + ```go func (tw *TimingWheel) moveTask(task baseEntry) { // timers: Map => 通过key获取 [positionEntry「pos, task」] @@ -142,7 +167,7 @@ func (tw *TimingWheel) moveTask(task baseEntry) { } timer := val.(*positionEntry) - // {delay < interval} => 延迟时间比一个时间格间隔还小,没有更小的刻度,说明任务应该立即执行 + // {delay < interval} => 延迟时间比一个时间格间隔还小,没有更小的刻度,说明任务应该立即执行 if task.delay < tw.interval { threading.GoSafe(func() { tw.execute(timer.item.key, timer.item.value) @@ -153,7 +178,7 @@ func (tw *TimingWheel) moveTask(task baseEntry) { pos, circle := tw.getPositionAndCircle(task.delay) if pos >= timer.pos { timer.item.circle = circle - // 记录前后的移动offset。为了后面过程重新入队 + // 记录前后的移动offset。为了后面过程重新入队 timer.item.diff = pos - timer.pos } else if circle > 0 { // 转移到下一层,将 circle 转换为 diff 一部分 @@ -175,31 +200,39 @@ func (tw *TimingWheel) moveTask(task baseEntry) { } ``` + 以上过程有以下几种情况: + - `delay < internal`:因为 < 单个时间精度,表示这个任务已经过期,需要马上执行 - 针对改变的 `delay`: - - `new >= old`:`` - - `newCircle > 0`:计算diff,并将 circle 转换为 下一层,故diff + numslots - - 如果只是单纯延迟时间缩短,则将老的task标记删除,重新加入list,等待下一轮loop被execute + - `new >= old`:`` + - `newCircle > 0`:计算diff,并将 circle 转换为 下一层,故diff + numslots + - 如果只是单纯延迟时间缩短,则将老的task标记删除,重新加入list,等待下一轮loop被execute + + ### Execute + 之前在初始化中,`run()` 中定时器的不断推进,推进的过程主要就是把 list中的 task 传给执行的 `execute func`。我们从定时器的执行开始看: + ```go // 定时器 「每隔 internal 会执行一次」 func (tw *TimingWheel) onTick() { - // 每次执行更新一下当前执行 tick 位置 + // 每次执行更新一下当前执行 tick 位置 tw.tickedPos = (tw.tickedPos + 1) % tw.numSlots - // 获取此时 tick位置 中的存储task的双向链表 + // 获取此时 tick位置 中的存储task的双向链表 l := tw.slots[tw.tickedPos] tw.scanAndRunTasks(l) } ``` + 紧接着是如何去执行 `execute`: + ```go func (tw *TimingWheel) scanAndRunTasks(l *list.List) { // 存储目前需要执行的task{key, value} [execute所需要的参数,依次传递给execute执行] @@ -207,7 +240,7 @@ func (tw *TimingWheel) scanAndRunTasks(l *list.List) { for e := l.Front(); e != nil; { task := e.Value.(*timingEntry) - // 标记删除,在 scan 中做真正的删除 「删除map的data」 + // 标记删除,在 scan 中做真正的删除 「删除map的data」 if task.removed { next := e.Next() l.Remove(e) @@ -216,7 +249,7 @@ func (tw *TimingWheel) scanAndRunTasks(l *list.List) { continue } else if task.circle > 0 { // 当前执行点已经过期,但是同时不在第一层,所以当前层即然已经完成了,就会降到下一层 - // 但是并没有修改 pos + // 但是并没有修改 pos task.circle-- e = e.Next() continue @@ -246,10 +279,13 @@ func (tw *TimingWheel) scanAndRunTasks(l *list.List) { } ``` + 具体的分支情况在注释中说明了,在看的时候可以和前面的 `moveTask()` 结合起来,其中 `circle` 下降,`diff` 的计算是关联两个函数的重点。 + 至于 `diff` 计算就涉及到 `pos, circle` 的计算: + ```go // interval: 4min, d: 60min, numSlots: 16, tickedPos = 15 // step = 15, pos = 14, circle = 0 @@ -261,29 +297,24 @@ func (tw *TimingWheel) getPositionAndCircle(d time.Duration) (pos int, circle in } ``` -> 上面的过程可以简化成下面: -> -> ```go -> steps = d / interval -> pos = step % numSlots - 1 -> circle = (step - 1) / numSlots -> ``` -## 总结 +上面的过程可以简化成下面: + +```go +steps = d / interval +pos = step % numSlots - 1 +circle = (step - 1) / numSlots +``` -1. `timingWheel` 靠定时器推动,时间前进的同时会取出**当前时间格**中 `list`「双向链表」的task,传递到 `execute` 中执行。因为是是靠 `internal` 固定时间刻度推进,可能就会出现:一个 60s 的task,`internal = 1s`,这样就会空跑59次loop。 -2. 而在扩展时间上,采取 `circle` 分层,这样就可以不断复用原有的 `numSlots` ,因为定时器在不断 `loop`,而执行可以把上层的 `slot` 下降到下层,在不断 `loop` 中就可以执行到上层的task。这样的设计可以在不创造额外的数据结构,突破长时间的限制。 -> 同时在 `go-zero` 中还有很多实用的组件工具,用好工具对于提升服务性能和开发效率都有很大的帮助,希望本篇文章能给大家带来一些收获。 +## 总结 + -同时欢迎大家使用 `go-zero` 并加入我们,[项目地址](https://github.com/tal-tech/go-zero) +`timingWheel` 靠定时器推动,时间前进的同时会取出**当前时间格**中 `list`「双向链表」的task,传递到 `execute` 中执行。 -wechat +而时间分隔上,时间轮有 `circle` 分层,这样就可以不断复用原有的 `numSlots` ,因为定时器在不断 `loop`,而执行可以把上层的 `slot` 下降到下层,在不断 `loop` 中就可以执行到上层的task。 -## 参考资料 -- [go-zero](https://github.com/tal-tech/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) +在 `go-zero` 中还有很多实用的组件工具,用好工具对于提升服务性能和开发效率都有很大的帮助,希望本篇文章能给大家带来一些收获。 diff --git a/go-zero.dev/cn/tips.md b/go-zero.dev/cn/tips.md new file mode 100644 index 00000000..0b6ae02e --- /dev/null +++ b/go-zero.dev/cn/tips.md @@ -0,0 +1,6 @@ +# 阅读须知 + +本文档从快速入门,详细项目开发流程,go-zero服务设计思想,goctl工具的使用等维度进行了介绍, +对于刚刚接触go或go-zero的同学需要把这些篇幅都看完才能有所了解,因此有些费力,这里建议大家阅读的方法。 +* 保持耐心跟着文档目录进行,文档是按照从简单到深入的渐进式过程编写的。 +* 在遇到问题或错误时,请一定记住多查[FAQ](faq.md)。 diff --git a/go-zero.dev/cn/tokenlimit.md b/go-zero.dev/cn/tokenlimit.md new file mode 100644 index 00000000..92912c15 --- /dev/null +++ b/go-zero.dev/cn/tokenlimit.md @@ -0,0 +1,152 @@ +# tokenlimit +本节将通过令牌桶限流(tokenlimit)来介绍其基本使用。 + +## 使用 + +```go +const ( + burst = 100 + rate = 100 + seconds = 5 +) + +store := redis.NewRedis("localhost:6379", "node", "") +fmt.Println(store.Ping()) +// New tokenLimiter +limiter := limit.NewTokenLimiter(rate, burst, store, "rate-test") +timer := time.NewTimer(time.Second * seconds) +quit := make(chan struct{}) +defer timer.Stop() +go func() { + <-timer.C + close(quit) +}() + +var allowed, denied int32 +var wait sync.WaitGroup +for i := 0; i < runtime.NumCPU(); i++ { + wait.Add(1) + go func() { + for { + select { + case <-quit: + wait.Done() + return + default: + if limiter.Allow() { + atomic.AddInt32(&allowed, 1) + } else { + atomic.AddInt32(&denied, 1) + } + } + } + }() +} + +wait.Wait() +fmt.Printf("allowed: %d, denied: %d, qps: %d\n", allowed, denied, (allowed+denied)/seconds) +``` + + +## tokenlimit + +从整体上令牌桶生产token逻辑如下: +- 用户配置的平均发送速率为r,则每隔1/r秒一个令牌被加入到桶中; +- 假设桶中最多可以存放b个令牌。如果令牌到达时令牌桶已经满了,那么这个令牌会被丢弃; +- 当流量以速率v进入,从桶中以速率v取令牌,拿到令牌的流量通过,拿不到令牌流量不通过,执行熔断逻辑; + + + +`go-zero` 在两类限流器下都采取 `lua script` 的方式,依赖redis可以做到分布式限流,`lua script`同时可以做到对 token 生产读取操作的原子性。 + +下面来看看 `lua script` 控制的几个关键属性: + +| argument | mean | +| --- | --- | +| ARGV[1] | rate 「每秒生成几个令牌」 | +| ARGV[2] | burst 「令牌桶最大值」 | +| ARGV[3] | now_time「当前时间戳」 | +| ARGV[4] | get token nums 「开发者需要获取的token数」 | +| KEYS[1] | 表示资源的tokenkey | +| KEYS[2] | 表示刷新时间的key | + + + +```lua +-- 返回是否可以活获得预期的token + +local rate = tonumber(ARGV[1]) +local capacity = tonumber(ARGV[2]) +local now = tonumber(ARGV[3]) +local requested = tonumber(ARGV[4]) + +-- fill_time:需要填满 token_bucket 需要多久 +local fill_time = capacity/rate +-- 将填充时间向下取整 +local ttl = math.floor(fill_time*2) + +-- 获取目前 token_bucket 中剩余 token 数 +-- 如果是第一次进入,则设置 token_bucket 数量为 令牌桶最大值 +local last_tokens = tonumber(redis.call("get", KEYS[1])) +if last_tokens == nil then + last_tokens = capacity +end + +-- 上一次更新 token_bucket 的时间 +local last_refreshed = tonumber(redis.call("get", KEYS[2])) +if last_refreshed == nil then + last_refreshed = 0 +end + +local delta = math.max(0, now-last_refreshed) +-- 通过当前时间与上一次更新时间的跨度,以及生产token的速率,计算出新的token数 +-- 如果超过 max_burst,多余生产的token会被丢弃 +local filled_tokens = math.min(capacity, last_tokens+(delta*rate)) +local allowed = filled_tokens >= requested +local new_tokens = filled_tokens +if allowed then + new_tokens = filled_tokens - requested +end + +-- 更新新的token数,以及更新时间 +redis.call("setex", KEYS[1], ttl, new_tokens) +redis.call("setex", KEYS[2], ttl, now) + +return allowed +``` + + +上述可以看出 `lua script` :只涉及对 token 操作,保证 token 生产合理和读取合理。 + + +## 函数分析 + + +![](https://cdn.nlark.com/yuque/0/2020/png/261626/1606107337223-7756ecdf-acb6-48c2-9ff5-959de01a1a03.png#align=left&display=inline&height=896&margin=%5Bobject%20Object%5D&originHeight=896&originWidth=2038&status=done&style=none&width=2038) + + +从上述流程中看出: + + +1. 有多重保障机制,保证限流一定会完成。 +1. 如果`redis limiter`失效,至少在进程内`rate limiter`兜底。 +1. 重试 `redis limiter` 机制保证尽可能地正常运行。 + + + +## 总结 + + +`go-zero` 中的 `tokenlimit` 限流方案适用于瞬时流量冲击,现实请求场景并不以恒定的速率。令牌桶相当预请求,当真实的请求到达不至于瞬间被打垮。当流量冲击到一定程度,则才会按照预定速率进行消费。 + + +但是生产`token`上,不能按照当时的流量情况作出动态调整,不够灵活,还可以进行进一步优化。此外可以参考[Token bucket WIKI](https://en.wikipedia.org/wiki/Token_bucket) 中提到分层令牌桶,根据不同的流量带宽,分至不同排队中。 + + +## 参考 + +- [go-zero tokenlimit](https://github.com/zeromicro/go-zero/blob/master/core/limit/tokenlimit.go) +- [Go-Redis 提供的分布式限流库](https://github.com/go-redis/redis_rate) + + + diff --git a/go-zero.dev/cn/tool-center.md b/go-zero.dev/cn/tool-center.md new file mode 100644 index 00000000..a6c6531f --- /dev/null +++ b/go-zero.dev/cn/tool-center.md @@ -0,0 +1,5 @@ +# 工具中心 +在go-zero中,提供了很多提高工程效率的工具,如api,rpc生成,在此基础之上,api文件的编写就显得那么的无力, +因为缺少了高亮,代码提示,模板生成等,本节将带你了解go-zero是怎么解决这些难题的,本节包含以下小节: +* [intellij插件](intellij.md) +* [vscode插件](vscode.md) \ No newline at end of file diff --git a/go-zero.dev/cn/trace.md b/go-zero.dev/cn/trace.md new file mode 100644 index 00000000..3d850e22 --- /dev/null +++ b/go-zero.dev/cn/trace.md @@ -0,0 +1,185 @@ +# go-zero链路追踪 + +## 序言 + +微服务架构中,调用链可能很漫长,从 `http` 到 `rpc` ,又从 `rpc` 到 `http` 。而开发者想了解每个环节的调用情况及性能,最佳方案就是 **全链路跟踪**。 + +追踪的方法就是在一个请求开始时生成一个自己的 `spanID` ,随着整个请求链路传下去。我们则通过这个 `spanID` 查看整个链路的情况和性能问题。 + +下面来看看 `go-zero` 的链路实现。 + +## 代码结构 + +- [spancontext](https://github.com/zeromicro/go-zero/blob/master/core/trace/spancontext.go) :保存链路的上下文信息「traceid,spanid,或者是其他想要传递的内容」 +- [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` 实现 + +![](https://static.gocn.vip/photo/2020/2f244477-4ed3-4ad1-8003-ff82cbe2f8a0.png?x-oss-process=image/resize,w_1920) + +## 概念 + +### SpanContext + +在介绍 `span` 之前,先引入 `context` 。SpanContext 保存了分布式追踪的上下文信息,包括 Trace id,Span id 以及其它需要传递到下游的内容。OpenTracing 的实现需要将 SpanContext 通过某种协议 进行传递,以将不同进程中的 Span 关联到同一个 Trace 上。对于 HTTP 请求来说,SpanContext 一般是采用 HTTP header 进行传递的。 + +下面是 `go-zero` 默认实现的 `spanContext` + +```go +type spanContext struct { + traceId string // TraceID 表示tracer的全局唯一ID + spanId string // SpanId 标示单个trace中某一个span的唯一ID,在trace中唯一 +} +``` + +同时开发者也可以实现 `SpanContext` 提供的接口方法,实现自己的上下文信息传递: + +```go +type SpanContext interface { + TraceId() string // get TraceId + SpanId() string // get SpanId + Visit(fn func(key, val string) bool) // 自定义操作TraceId,SpanId +} +``` + +### Span + +一个 REST 调用或者数据库操作等,都可以作为一个 `span` 。 `span` 是分布式追踪的最小跟踪单位,一个 Trace 由多段 Span 组成。追踪信息包含如下信息: + +```go +type Span struct { + ctx spanContext // 传递的上下文 + serviceName string // 服务名 + operationName string // 操作 + startTime time.Time // 开始时间戳 + flag string // 标记开启trace是 server 还是 client + children int // 本 span fork出来的 childsnums +} +``` + +从 `span` 的定义结构来看:在微服务中, 这就是一个完整的子调用过程,有调用开始 `startTime` ,有标记自己唯一属性的上下文结构 `spanContext` 以及 fork 的子节点数。 + +## 实例应用 + +在 `go-zero` 中http,rpc中已经作为内置中间件集成。我们以 [http](https://github.com/zeromicro/go-zero/blob/master/rest/handler/tracinghandler.go) ,[rpc](https://github.com/zeromicro/go-zero/blob/master/zrpc/internal/clientinterceptors/tracinginterceptor.go) 中,看看 `tracing` 是怎么使用的: + +### HTTP + +```go +func TracingHandler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // **1** + carrier, err := trace.Extract(trace.HttpFormat, r.Header) + // ErrInvalidCarrier means no trace id was set in http header + if err != nil && err != trace.ErrInvalidCarrier { + logx.Error(err) + } + + // **2** + ctx, span := trace.StartServerSpan(r.Context(), carrier, sysx.Hostname(), r.RequestURI) + defer span.Finish() + // **5** + r = r.WithContext(ctx) + + next.ServeHTTP(w, r) + }) +} + +func StartServerSpan(ctx context.Context, carrier Carrier, serviceName, operationName string) ( + context.Context, tracespec.Trace) { + span := newServerSpan(carrier, serviceName, operationName) + // **4** + return context.WithValue(ctx, tracespec.TracingKey, span), span +} + +func newServerSpan(carrier Carrier, serviceName, operationName string) tracespec.Trace { + // **3** + traceId := stringx.TakeWithPriority(func() string { + if carrier != nil { + return carrier.Get(traceIdKey) + } + return "" + }, func() string { + return stringx.RandId() + }) + spanId := stringx.TakeWithPriority(func() string { + if carrier != nil { + return carrier.Get(spanIdKey) + } + return "" + }, func() string { + return initSpanId + }) + + return &Span{ + ctx: spanContext{ + traceId: traceId, + spanId: spanId, + }, + serviceName: serviceName, + operationName: operationName, + startTime: timex.Time(), + // 标记为server + flag: serverFlag, + } +} +``` + +1. 将 header -> carrier,获取 header 中的traceId等信息 +2. 开启一个新的 span,并把**「traceId,spanId」**封装在context中 +3. 从上述的 carrier「也就是header」获取traceId,spanId + - 看header中是否设置 + - 如果没有设置,则随机生成返回 +4. 从 `request` 中产生新的ctx,并将相应的信息封装在 ctx 中,返回 +5. 从上述的 context,拷贝一份到当前的 `request` + +![](https://static.gocn.vip/photo/2020/a30daba2-ad12-477c-8ce5-131ef1cc3e76.png?x-oss-process=image/resize,w_1920) + +这样就实现了 `span` 的信息随着 `request` 传递到下游服务。 + +### RPC + +在 rpc 中存在 `client, server` ,所以从 `tracing` 上也有 `clientTracing, serverTracing` 。 `serveTracing` 的逻辑基本与 http 的一致,来看看 `clientTracing` 是怎么使用的? + +```go +func TracingInterceptor(ctx context.Context, method string, req, reply interface{}, + cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { + // open clientSpan + ctx, span := trace.StartClientSpan(ctx, cc.Target(), method) + defer span.Finish() + + var pairs []string + span.Visit(func(key, val string) bool { + pairs = append(pairs, key, val) + return true + }) + // **3** 将 pair 中的data以map的形式加入 ctx + ctx = metadata.AppendToOutgoingContext(ctx, pairs...) + + return invoker(ctx, method, req, reply, cc, opts...) +} + +func StartClientSpan(ctx context.Context, serviceName, operationName string) (context.Context, tracespec.Trace) { + // **1** + if span, ok := ctx.Value(tracespec.TracingKey).(*Span); ok { + // **2** + return span.Fork(ctx, serviceName, operationName) + } + + return ctx, emptyNoopSpan +} +``` + +1. 获取上游带下来的 span 上下文信息 +2. 从获取的 span 中创建新的 ctx,span「继承父span的traceId」 +3. 将生成 span 的data加入ctx,传递到下一个中间件,流至下游 + +## 总结 + +`go-zero` 通过拦截请求获取链路traceID,然后在中间件函数入口会分配一个根Span,然后在后续操作中会分裂出子Span,每个span都有自己的具体的标识,Finsh之后就会汇集在链路追踪系统中。开发者可以通过 `ELK` 工具追踪 `traceID` ,看到整个调用链。 + +同时 `go-zero` 并没有提供整套 `trace` 链路方案,开发者可以封装 `go-zero` 已有的 `span` 结构,做自己的上报系统,接入 `jaeger, zipkin` 等链路追踪工具。 + +## 参考 + +- [go-zero trace](https://github.com/zeromicro/go-zero/tree/master/core/trace) \ No newline at end of file diff --git a/go-zero.dev/cn/vscode.md b/go-zero.dev/cn/vscode.md new file mode 100644 index 00000000..2e5ef0d4 --- /dev/null +++ b/go-zero.dev/cn/vscode.md @@ -0,0 +1,47 @@ +# vs code 插件 +该插件可以安装在 1.46.0+ 版本的 Visual Studio Code 上,首先请确保你的 Visual Studio Code 版本符合要求,并已安装 goctl 命令行工具。如果尚未安装 Visual Studio Code,请安装并打开 Visual Studio Code。 导航到“扩展”窗格,搜索 goctl 并安装此扩展(发布者ID为 “xiaoxin-technology.goctl”)。 + +Visual Studio Code 扩展使用请参考[这里](https://code.visualstudio.com/docs/editor/extension-gallery)。 + +## 功能列表 + +已实现功能 + +* 语法高亮 +* 跳转到定义/引用 +* 代码格式化 +* 代码块提示 + +未实现功能: + +* 语法错误检查 +* 跨文件代码跳转 +* goctl 命令行调用 + +### 语法高亮 + +### 代码跳转 + +![jump](./resource/jump.gif) + +### 代码格式化 + +调用 goctl 命令行格式化工具,使用前请确认 goctl 已加入 `$PATH` 且有可执行权限 + +### 代码块提示 + +#### info 代码块 + +![info](./resource/info.gif) + +#### type 代码块 + +![type](./resource/type.gif) + +#### service 代码块 + +![type](./resource/service.gif) + +#### handler 代码块 + +![type](./resource/handler.gif) diff --git a/go-zero.dev/cn/wechat.md b/go-zero.dev/cn/wechat.md new file mode 100644 index 00000000..24ab835b --- /dev/null +++ b/go-zero.dev/cn/wechat.md @@ -0,0 +1,27 @@ +# 公众号 +微服务实战是go-zero的官方公众号,在这里会发布最新的go-zero最佳实践,同步go夜读、go开源说、GopherChina、腾讯云开发者大会等多渠道关于go-zero的最新技术和资讯。 + + + + + + + + + + + + +
公众号名称公众号作者公众号二维码
微服务实战kevwan微服务实践
+ +# 干货 +这里列举一些干货,想要收获更多go-zero最佳实践干货,可以关注公众号获取最新动态。 +* [《一文读懂云原生 go-zero 微服务框架》](https://mp.weixin.qq.com/s/gszj3-fwfcof5Tt2Th4dFA) +* [《你还在手撕微服务?快试试 go-zero 的微服务自动生成》](https://mp.weixin.qq.com/s/Qvi-g3obgD_FVJ7CK3O56w) +* [《最简单的Go Dockerfile编写姿势,没有之一!》](https://mp.weixin.qq.com/s/VLBiIbZStKhb7uth1ndgQQ) +* [《通过MapReduce降低服务响应时间》](https://mp.weixin.qq.com/s/yxXAIK1eC_X22DH4ssZSag) +* [《微服务过载保护原理与实战](https://mp.weixin.qq.com/s/CWzf6CY2R12Xd-rIYVvdPQ) +* [《最简单的 K8S 部署文件编写姿势,没有之一!》](https://mp.weixin.qq.com/s/1GOMxlI8ocOL3U_I2TKPzQ) +* [《go-zero 如何应对海量定时/延迟任务?》](https://mp.weixin.qq.com/s/CiZ5SpuT-VN8V9wil8_iGg) +* [《go-zero 如何扛住流量冲击(一)》](https://mp.weixin.qq.com/s/xnJIm3asMncBfbtXo22sZw) +* [《服务自适应降载保护设计》](https://mp.weixin.qq.com/s/cgjCL59e3CDWhsxzwkuKBg) \ No newline at end of file diff --git a/doc/zrpc.md b/go-zero.dev/cn/zrpc.md similarity index 96% rename from doc/zrpc.md rename to go-zero.dev/cn/zrpc.md index 3a9c0fce..6eae59f8 100644 --- a/doc/zrpc.md +++ b/go-zero.dev/cn/zrpc.md @@ -2,7 +2,7 @@ # 企业级RPC框架zRPC -近期比较火的开源项目[go-zero](https://github.com/tal-tech/go-zero)是一个集成了各种工程实践的包含了Web和RPC协议的功能完善的微服务框架,今天我们就一起来分析一下其中的RPC部分[zRPC](https://github.com/tal-tech/go-zero/tree/master/zrpc)。 +近期比较火的开源项目[go-zero](https://github.com/zeromicro/go-zero)是一个集成了各种工程实践的包含了Web和RPC协议的功能完善的微服务框架,今天我们就一起来分析一下其中的RPC部分[zRPC](https://github.com/zeromicro/go-zero/tree/master/zrpc)。 zRPC底层依赖gRPC,内置了服务注册、负载均衡、拦截器等模块,其中还包括自适应降载,自适应熔断,限流等微服务治理方案,是一个简单易用的可直接用于生产的企业级RPC框架。 @@ -209,7 +209,7 @@ K: 倍值 (Google SRE推荐值为2) 可以通过修改K的值来修改熔断发生的激进程度,降低K的值会使得自适应熔断算法更加激进,增加K的值则自适应熔断算法变得不再那么激进 -[熔断拦截器](https://github.com/tal-tech/go-zero/blob/master/zrpc/internal/clientinterceptors/breakerinterceptor.go)定义如下: +[熔断拦截器](https://github.com/zeromicro/go-zero/blob/master/zrpc/internal/clientinterceptors/breakerinterceptor.go)定义如下: ```go func BreakerInterceptor(ctx context.Context, method string, req, reply interface{}, @@ -281,7 +281,7 @@ func (b *googleBreaker) doReq(req func() error, fallback func(err error) error, 服务监控是了解服务当前运行状态以及变化趋势的重要手段,监控依赖于服务指标的收集,通过prometheus进行监控指标的收集是业界主流方案,zRPC中也采用了prometheus来进行指标的收集 -[prometheus拦截器](https://github.com/tal-tech/go-zero/blob/master/zrpc/internal/serverinterceptors/prometheusinterceptor.go)定义如下: +[prometheus拦截器](https://github.com/zeromicro/go-zero/blob/master/zrpc/internal/serverinterceptors/prometheusinterceptor.go)定义如下: 这个拦截器主要是对服务的监控指标进行收集,这里主要是对RPC方法的耗时和调用错误进行收集,这里主要使用了Prometheus的Histogram和Counter数据类型 diff --git a/go-zero.dev/en/README.md b/go-zero.dev/en/README.md new file mode 100644 index 00000000..5ca30ffb --- /dev/null +++ b/go-zero.dev/en/README.md @@ -0,0 +1,223 @@ + + +# go-zero + +[![Go](https://github.com/zeromicro/go-zero/workflows/Go/badge.svg?branch=master)](https://github.com/zeromicro/go-zero/actions) +[![codecov](https://codecov.io/gh/tal-tech/go-zero/branch/master/graph/badge.svg)](https://codecov.io/gh/tal-tech/go-zero) +[![Go Report Card](https://goreportcard.com/badge/github.com/tal-tech/go-zero)](https://goreportcard.com/report/github.com/tal-tech/go-zero) +[![Release](https://img.shields.io/github/v/release/tal-tech/go-zero.svg?style=flat-square)](https://github.com/zeromicro/go-zero) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + +## 0. what is go-zero + +go-zero is a web and rpc framework that with lots of engineering practices builtin. It’s born to ensure the stability of the busy services with resilience design, and has been serving sites with tens of millions users for years. + +go-zero contains simple API description syntax and code generation tool called `goctl`. You can generate Go, iOS, Android, Kotlin, Dart, TypeScript, JavaScript from .api files with `goctl`. + +Advantages of go-zero: + +* improve the stability of the services with tens of millions of daily active users +* builtin chained timeout control, concurrency control, rate limit, adaptive circuit breaker, adaptive load shedding, even no configuration needed +* builtin middlewares also can be integrated into your frameworks +* simple API syntax, one command to generate couple of different languages +* auto validate the request parameters from clients +* plenty of builtin microservice management and concurrent toolkits + +Architecture + +## 1. Backgrounds of go-zero + +At the beginning of 2018, we decided to re-design our system, from monolithic architecture with Java+MongoDB to microservice architecture. After researches and comparison, we chose to: + +* Golang based + * great performance + * simple syntax + * proven engineering efficiency + * extreme deployment experience + * less server resource consumption +* Self-designed microservice architecture + * I have rich experience on designing microservice architectures + * easy to location the problems + * easy to extend the features + +## 2. Design considerations on go-zero + +By designing the microservice architecture, we expected to ensure the stability, as well as the productivity. And from just the beginning, we have the following design principles: + +* keep it simple +* high availability +* stable on high concurrency +* easy to extend +* resilience design, failure-oriented programming +* try best to be friendly to the business logic development, encapsulate the complexity +* one thing, one way + +After almost half a year, we finished the transfer from monolithic system to microservice system, and deployed on August 2018. The new system guaranteed the business growth, and the system stability. + +## 3. The implementation and features of go-zero + +go-zero is a web and rpc framework that integrates lots of engineering practices. The features are mainly listed below: + +* powerful tool included, less code to write +* simple interfaces +* fully compatible with net/http +* middlewares are supported, easy to extend +* high performance +* failure-oriented programming, resilience design +* builtin service discovery, load balancing +* builtin concurrency control, adaptive circuit breaker, adaptive load shedding, auto trigger, auto recover +* auto validation of API request parameters +* chained timeout control +* auto management of data caching +* call tracing, metrics and monitoring +* high concurrency protected + +As below, go-zero protects the system with couple layers and mechanisms: + +![Resilience](https://raw.githubusercontent.com/tal-tech/zero-doc/main/doc/images/resilience-en.png) + +## 4. Future development plans of go-zero + +* auto generate API mock server, make the client debugging easier +* auto generate the simple integration test for the server side just from the .api files + +## 5. Installation + +Run the following command under your project: + +```shell +go get -u github.com/tal-tech/go-zero +``` + +## 6. Quick Start + +0. full examples can be checked out from below: + + [Rapid development of microservice systems](https://github.com/tal-tech/zero-doc/blob/main/doc/shorturl-en.md) + + [Rapid development of microservice systems - multiple RPCs](https://github.com/tal-tech/zero-doc/blob/main/doc/bookstore-en.md) + +1. install goctl + + `goctl`can be read as `go control`. `goctl` means not to be controlled by code, instead, we control it. The inside `go` is not `golang`. At the very beginning, I was expecting it to help us improve the productivity, and make our lives easier. + + ```shell + GO111MODULE=on go get -u github.com/tal-tech/go-zero/tools/goctl + ``` + + make sure goctl is executable. + +2. create the API file, like greet.api, you can install the plugin of goctl in vs code, api syntax is supported. + + ```go + type Request struct { + Name string `path:"name,options=you|me"` // parameters are auto validated + } + + type Response struct { + Message string `json:"message"` + } + + service greet-api { + @handler GreetHandler + get /greet/from/:name(Request) returns (Response); + } + ``` + + the .api files also can be generate by goctl, like below: + + ```shell + goctl api -o greet.api + ``` + +3. generate the go server side code + + ```shell + goctl api go -api greet.api -dir greet + ``` + + the generated files look like: + + ``` + ├── greet + │   ├── etc + │   │   └── greet-api.yaml // configuration file + │   ├── greet.go // main file + │   └── internal + │   ├── config + │   │   └── config.go // configuration definition + │   ├── handler + │   │   ├── greethandler.go // get/put/post/delete routes are defined here + │   │   └── routes.go // routes list + │   ├── logic + │   │   └── greetlogic.go // request logic can be written here + │   ├── svc + │   │   └── servicecontext.go // service context, mysql/redis can be passed in here + │   └── types + │   └── types.go // request/response defined here + └── greet.api // api description file + ``` + + the generated code can be run directly: + + ```shell + cd greet + go mod init + go mod tidy + go run greet.go -f etc/greet-api.yaml + ``` + + by default, it’s listening on port 8888, while it can be changed in configuration file. + + you can check it by curl: + + ```shell + curl -i http://localhost:8888/greet/from/you + ``` + + the response looks like: + + ```http + HTTP/1.1 200 OK + Date: Sun, 30 Aug 2020 15:32:35 GMT + Content-Length: 0 + ``` + +4. Write the business logic code + + * the dependencies can be passed into the logic within servicecontext.go, like mysql, reds etc. + * add the logic code in logic package according to .api file + +5. Generate code like Java, TypeScript, Dart, JavaScript etc. just from the api file + + ```shell + goctl api java -api greet.api -dir greet + goctl api dart -api greet.api -dir greet + ... + ``` + +## 7. Benchmark + +![benchmark](https://raw.githubusercontent.com/tal-tech/zero-doc/main/doc/images/benchmark.png) + +[Checkout the test code](https://github.com/smallnest/go-web-framework-benchmark) + +## 8. Documents (adding) + +* [Rapid development of microservice systems](https://github.com/tal-tech/zero-doc/blob/main/doc/shorturl-en.md) +* [Rapid development of microservice systems - multiple RPCs](https://github.com/tal-tech/zero-doc/blob/main/docs/zero/bookstore-en.md) +* [Examples](https://github.com/zeromicro/zero-examples) + +## 9. Important notes + +* Use grpc 1.29.1, because etcd lib doesn’t support latter versions. + + `google.golang.org/grpc v1.29.1` + +* For protobuf compatibility, use `protocol-gen@v1.3.2`. + + ` go get -u github.com/golang/protobuf/protoc-gen-go@v1.3.2` + +## 10. Chat group + +Join the chat via https://join.slack.com/t/go-zeroworkspace/shared_invite/zt-m39xssxc-kgIqERa7aVsujKNj~XuPKg diff --git a/go-zero.dev/en/about-us.md b/go-zero.dev/en/about-us.md new file mode 100644 index 00000000..b3860771 --- /dev/null +++ b/go-zero.dev/en/about-us.md @@ -0,0 +1,21 @@ +# About Us + +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +## Go-Zero +go-zero is a web and rpc framework that integrates various engineering practices. Through flexible design, the stability of the large concurrent server is guaranteed, and it has withstood full actual combat tests. + +go-zero contains a minimalist API definition and generation tool goctl, which can generate Go, iOS, Android, Kotlin, Dart, TypeScript, JavaScript code with one click according to the defined api file, and run it directly. + +## Go-Zero's Author +[kevwan](https://github.com/kevwan) + +**kevwan** is the XiaoHeiBan’s R&D person in charge and a senior technical expert in TAL, whhas 14 years of R&D team management experience, 16 years of architecture design experience, 20 years of engineering practical experience, responsible for the architecture design of many large-scale projects, and has been in partnership for many times (acquired ), Lecturer of Gopher China Conference, Lecturer of Tencent Cloud Developer Conference. + +## Go-Zero Members +As of February 2021, go-zero currently has 30 team developers and 50+ community members. + +## Go-Zero Community +We currently have more than 3,000 community members. Here, you can discuss any go-zero technology, feedback on issues, get the latest go-zero information, and the technical experience shared by the big guys every day. + diff --git a/go-zero.dev/en/api-coding.md b/go-zero.dev/en/api-coding.md new file mode 100644 index 00000000..2215fe6c --- /dev/null +++ b/go-zero.dev/en/api-coding.md @@ -0,0 +1,57 @@ +# API File Coding + +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +## Create file +```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) +} +``` +## Generate api service +### By goctl executable file + +```shell +$ cd book/service/user/cmd/api +$ goctl api go -api user.api -dir . +``` +```text +Done. +``` + +### By Intellij Plugin + +Right-click on the `user.api` file, and then click to enter `New`->`Go Zero`->`Api Code`, enter the target directory selection, that is, the target storage directory of the api source code, the default is the directory where user.api is located, select Click OK after finishing the list. + +![ApiGeneration](https://zeromicro.github.io/go-zero-pages/resource/goctl-api.png) +![ApiGenerationDirectorySelection](https://zeromicro.github.io/go-zero-pages/resource/goctl-api-select.png) + +### By Keymap + +Open user.api, enter the editing area, use the shortcut key `Command+N` (for macOS) or `alt+insert` (for windows), select `Api Code`, and also enter the directory selection pop-up window, after selecting the directory Just click OK. + +# Guess you wants +* [API IDL](api-grammar.md) +* [API Commands](goctl-api.md) +* [API Directory Structure](api-dir.md) \ No newline at end of file diff --git a/go-zero.dev/en/api-config.md b/go-zero.dev/en/api-config.md new file mode 100644 index 00000000..7959fd5f --- /dev/null +++ b/go-zero.dev/en/api-config.md @@ -0,0 +1,112 @@ +# API configuration + +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +The api configuration controls various functions in the api service, including but not limited to the service listening address, port, environment configuration, log configuration, etc. Let's take a simple configuration to see what the common configurations in the api do. + +## Configuration instructions +Through the yaml configuration, we will find that there are many parameters that we are not aligned with config. This is because many of the config definitions are labeled with `optional` or `default`. For `optional` options, you can choose according to your own Need to determine whether it needs to be set. For the `default` tag, if you think the default value is enough, you don't need to set it. Generally, the value in `default` basically does not need to be modified and can be considered as a best practice value. + +### Config + +```go +type Config struct{ + rest.RestConf // rest api configuration + Auth struct { // jwt authentication configuration + AccessSecret string // jwt key + AccessExpire int64 // jwt expire, unit: second + } + Mysql struct { // database configuration, in addition to mysql, there may be other databases such as mongo + DataSource string // mysql datasource, which satisfies the format of user:password@tcp(ip:port)db?queries + } + CacheRedis cache.CacheConf // redis cache + UserRpc zrpc.RpcClientConf // rpc client configuration +} +``` + +### rest.RestConf +The basic configuration of api service, including monitoring address, monitoring port, certificate configuration, current limit, fusing parameters, timeout parameters and other controls, expand it, we can see: +```go +service.ServiceConf // service configuration +Host string `json:",default=0.0.0.0"` // http listening ip, default 0.0.0.0 +Port int // http listening port, required +CertFile string `json:",optional"` // https certificate file, optional +KeyFile string `json:",optional"` // https private key file, optional +Verbose bool `json:",optional"` // whether to print detailed http request log +MaxConns int `json:",default=10000"` // http can accept the maximum number of requests at the same time (current limit), the default is 10000 +MaxBytes int64 `json:",default=1048576,range=[0:8388608]"` // http can accept the maximum Content Length of the request, the default is 1048576, and the set value cannot be between 0 and 8388608 +// milliseconds +Timeout int64 `json:",default=3000"` // timeout duration control, unit: milliseconds, default 3000 +CpuThreshold int64 `json:",default=900,range=[0:1000]"` // CPU load reduction threshold, the default is 900, the allowable setting range is 0 to 1000 +Signature SignatureConf `json:",optional"` // signature configuration +``` + +### service.ServiceConf +```go +type ServiceConf struct { + Name string // service name + Log logx.LogConf // log configuration + Mode string `json:",default=pro,options=dev|test|pre|pro"` // service environment, dev-development environment, test-test environment, pre-pre-release environment, pro-formal environment + MetricsUrl string `json:",optional"` // index report interface address, this address needs to support post json + Prometheus prometheus.Config `json:",optional"` // prometheus configuration +} +``` + +### logx.LogConf +```go +type LogConf struct { + ServiceName string `json:",optional"` // service name + Mode string `json:",default=console,options=console|file|volume"` // Log mode, console-output to console, file-output to the current server (container) file, volume-output docker hangs in the file + Path string `json:",default=logs"` // Log storage path + Level string `json:",default=info,options=info|error|severe"` // Log level + Compress bool `json:",optional"` // whether to enable gzip compression + KeepDays int `json:",optional"` // log retention days + StackCooldownMillis int `json:",default=100"` // log write interval +} +``` + +### prometheus.Config +```go +type Config struct { + Host string `json:",optional"` // prometheus monitor host + Port int `json:",default=9101"` // prometheus listening port + Path string `json:",default=/metrics"` // report address +} +``` + +### SignatureConf +```go +SignatureConf struct { + Strict bool `json:",default=false"` // Whether it is Strict mode, if it is, Private Keys is required + Expiry time.Duration `json:",default=1h"` // Validity period, default is 1 hour + PrivateKeys []PrivateKeyConf // Signing key related configuration +} +``` + +### PrivateKeyConf +```go +PrivateKeyConf struct { + Fingerprint string // Fingerprint configuration + KeyFile string // Key configuration +} +``` + +### cache.CacheConf +```go +ClusterConf []NodeConf + +NodeConf struct { + redis.RedisConf + Weight int `json:",default=100"` // Weights +} +``` + +### redis.RedisConf +```go +RedisConf struct { + Host string // redis address + Type string `json:",default=node,options=node|cluster"` // redis type + Pass string `json:",optional"` // redis password +} +``` diff --git a/go-zero.dev/en/api-dir.md b/go-zero.dev/en/api-dir.md new file mode 100644 index 00000000..cdbe1f0d --- /dev/null +++ b/go-zero.dev/en/api-dir.md @@ -0,0 +1,27 @@ +# API directory introduction + +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +```text +. +├── etc +│ └── greet-api.yaml // yaml configuration file +├── go.mod // go module file +├── greet.api // api interface description language file +├── greet.go // main function entry +└── internal + ├── config + │ └── config.go // configuration declaration type + ├── handler // routing and handler forwarding + │ ├── greethandler.go + │ └── routes.go + ├── logic // business logic + │ └── greetlogic.go + ├── middleware // middleware file + │ └── greetmiddleware.go + ├── svc // the resource pool that logic depends on + │ └── servicecontext.go + └── types // The struct of request and response is automatically generated according to the api, and editing is not recommended + └── types.go +``` \ No newline at end of file diff --git a/go-zero.dev/en/api-grammar.md b/go-zero.dev/en/api-grammar.md new file mode 100644 index 00000000..64676cf6 --- /dev/null +++ b/go-zero.dev/en/api-grammar.md @@ -0,0 +1,743 @@ +# API syntax +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +## API IDL example + +```go +/** + * api syntax example and syntax description + */ + +// api syntax version +syntax = "v1" + +// import literal +import "foo.api" + +// import group +import ( + "bar.api" + "foo/bar.api" +) +info( + author: "anqiansong" + date: "2020-01-08" + desc: "api syntax example and syntax description" +) + +// 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 structure + +* syntax statement +* import syntax block +* info syntax block +* type syntax block +* service syntax block +* hidden channel + +> [!TIP] +> In the above grammatical structure, grammatically speaking, each grammar block can be declared anywhere in the .api file according to the grammatical block.> But in order to improve reading efficiency, we suggest to declare in the above order, because it may be in the future Strict mode is used to control the order of syntax blocks. + +### syntax statement + +syntax is a newly added grammatical structure, the introduction of the grammar can solve: + +* Quickly locate the problematic grammatical structure of the api version +* Syntax analysis for the version +* Prevent the big version upgrade of api syntax from causing backward compatibility + +> **[!WARNING] +> The imported api must be consistent with the syntax version of the main api. + +**Grammar definition** + +```antlrv4 +'syntax'={checkVersion(p)}STRING +``` + +**Grammar description** + +syntax: Fixed token, marking the beginning of a syntax structure + +checkVersion: Custom go method to detect whether `STRING` is a legal version number. The current detection logic is that STRING must meet the regularity of `(?m)"v[1-9][0-9]"`. + +STRING: A string of English double quotes, such as "v1" + +An api grammar file can only have 0 or 1 syntax statement. If there is no syntax, the default version is `v1` + +**Examples of correct syntax** ✅ + +eg1: Irregular writing + +```api +syntax="v1" +``` + +eg2: Standard writing (recommended) + +```api +syntax = "v2" +``` + +**Examples of incorrect syntax** ❌ + +eg1: + +```api +syntax = "v0" +``` + +eg2: + +```api +syntax = v1 +``` + +eg3: + +```api +syntax = "V1" +``` + +## Import syntax block + +As the business scale increases, there are more and more structures and services defined in the api. +All the grammatical descriptions are in one api file. This is a problem, and it will greatly increase the difficulty of reading and maintenance. +Import The grammar block can help us solve this problem. By splitting the api file, different api files are declared according to certain rules, +which can reduce the difficulty of reading and maintenance. + +> **[!WARNING] +> Import here does not include package declarations like golang, it is just the introduction of a file path. After the final analysis, all the declarations will be gathered into a spec.Spec. +> You cannot import multiple identical paths, otherwise it will cause parsing errors. + +**Grammar definition** + +```antlrv4 +'import' {checkImportValue(p)}STRING +|'import' '(' ({checkImportValue(p)}STRING)+ ')' +``` + +**Grammar description** + +import: fixed token, marking the beginning of an import syntax + +checkImportValue: Custom go method to detect whether `STRING` is a legal file path. The current detection logic is that STRING must satisfy `(?m)"(?[az AZ 0-9_-])+\. api"` regular. + +STRING: A string of English double quotes, such as "foo.api" + +**Examples of correct syntax** ✅ + +eg: + +```api +import "foo.api" +import "foo/bar.api" + +import( + "bar.api" + "foo/bar/foo.api" +) +``` + +**Examples of incorrect syntax** ❌ + +eg: + +```api +import foo.api +import "foo.txt" +import ( + bar.api + bar.api +) +``` + +## info syntax block + +The info grammar block is a grammar body that contains multiple key-value pairs. +Its function is equivalent to the description of an api service. The parser will map it to spec.Spec for translation into other languages ​​(golang, java, etc.) +Is the meta element that needs to be carried. If it is just a description of the current api, without considering its translation to other languages, +you can use simple multi-line comments or java-style documentation comments. For comment descriptions, please refer to the hidden channels below. + +> **[!WARNING] +> Duplicate keys cannot be used, each api file can only have 0 or 1 info syntax block + +**Grammar definition** + +```antlrv4 +'info' '(' (ID {checkKeyValue(p)}VALUE)+ ')' +``` + +**Grammar description** + +info: fixed token, marking the beginning of an info syntax block + +checkKeyValue: Custom go method to check whether `VALUE` is a legal value. + +VALUE: The value corresponding to the key, which can be any character after a single line except'\r','\n',''. For multiple lines, please wrap it with "", but it is strongly recommended that everything be wrapped with "" + +**Examples of correct syntax** ✅ + +eg1:Irregular writing + +```api +info( +foo: foo value +bar:"bar value" + desc:"long long long long +long long text" +) +``` + +eg2:Standard writing (recommended) + +```api +info( + foo: "foo value" + bar: "bar value" + desc: "long long long long long long text" +) +``` + +**Examples of incorrect syntax** ❌ + +eg1:No key-value + +```api +info() +``` + +eg2:Does not contain colon + +```api +info( + foo value +) +``` + +eg3:key-value does not wrap + +```api +info(foo:"value") +``` + +eg4:No key + +```api +info( + : "value" +) +``` + +eg5:Illegal key + +```api +info( + 12: "value" +) +``` + +eg6:Remove the old version of multi-line syntax + +```api +info( + foo: > + some text + < +) +``` + +## type syntax block + +In the api service, we need to use a structure (class) as the carrier of the request body and the response body. +Therefore, we need to declare some structures to accomplish this. The type syntax block evolved from the type of golang. +Of course It also retains some of the characteristics of golang type, and the following golang characteristics are used: + +* Keep the built-in data types of golang `bool`,`int`,`int8`,`int16`,`int32`,`int64`,`uint`,`uint8`,`uint16`,`uint32`,`uint64`,`uintptr` + ,`float32`,`float64`,`complex64`,`complex128`,`string`,`byte`,`rune`, +* Compatible with golang struct style declaration +* Keep golang keywords + +> **[!WARNING]️ +> * Does not support alias +> * Does not support `time.Time` data type +> * Structure name, field name, cannot be a golang keyword + +**Grammar definition** + +Since it is similar to golang, it will not be explained in detail. Please refer to the typeSpec definition in [ApiParser.g4](https://github.com/zeromicro/go-zero/blob/master/tools/goctl/api/parser/g4/ApiParser.g4) for the specific syntax definition. + +**Grammar description** + +Refer to golang writing + +**Examples of correct syntax** ✅ + +eg1:Irregular writing + +```api +type Foo struct{ + Id int `path:"id"` // ① + Foo int `json:"foo"` +} + +type Bar struct{ + // Non-exported field + bar int `form:"bar"` +} + +type( + // Non-derived structure + fooBar struct{ + FooBar int + } +) +``` + +eg2: Standard writing (recommended) + +```api +type Foo{ + Id int `path:"id"` + Foo int `json:"foo"` +} + +type Bar{ + Bar int `form:"bar"` +} + +type( + FooBar{ + FooBar int + } +) +``` + +**Examples of incorrect syntax** ❌ + +eg + +```api +type Gender int // not support + +// Non-struct token +type Foo structure{ + CreateTime time.Time // Does not support time.Time +} + +// golang keyword var +type var{} + +type Foo{ + // golang keyword interface + Foo interface +} + + +type Foo{ + foo int + // The map key must have the built-in data type of golang + m map[Bar]string +} +``` + +> [!NOTE] ① +> The tag definition is the same as the json tag syntax in golang. In addition to the json tag, go-zero also provides some other tags to describe the fields, +> See the table below for details. + +* tag table + + + + + + + + + + + + + +
tag key Description ProviderEffective Coverage Example
json Json serialization tag golang request、response json:"fooo"
path Routing path, such as/foo/:id go-zero request path:"id"
form Mark that the request body is a form (in the POST method) or a query (in the GET method)/search?name=keyword) go-zero request form:"name"
+* tag modifier + + Common parameter verification description + + + + + + + + + + + + + + + + +
tag key Description Provider Effective Coverage Example
optional Define the current field as an optional parameter go-zero request json:"name,optional"
options Define the enumeration value of the current field, separated by a vertical bar | go-zero request json:"gender,options=male"
default Define the default value of the current field go-zero request json:"gender,default=male"
range Define the value range of the current field go-zero request json:"age,range=[0:120]"
+ + > [!TIP] + > The tag modifier needs to be separated by a quotation comma after the tag value + +## service syntax block + +The service syntax block is used to define api services, including service name, service metadata, middleware declaration, routing, handler, etc. + +> **[!WARNING]️ +> * The service name of the main api and the imported api must be the same, and there must be no ambiguity in the service name. +> * The handler name cannot be repeated +> * The name of the route (request method + request path) cannot be repeated +> * The request body must be declared as a normal (non-pointer) struct, and the response body has been processed for forward compatibility. Please refer to the following description for details +> + +**Grammar definition** + +```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)?)))+; +``` + +**Grammar description** + +serviceSpec: Contains an optional syntax block `atServer` and `serviceApi` syntax block, which follow the sequence mode (the service must be written in order, otherwise it will be parsed incorrectly) + +atServer: Optional syntax block, defining server metadata of the key-value structure,'@server' indicates the beginning of this server syntax block, which can be used to describe serviceApi or route syntax block, and it has some special keys when it is used to describe different syntax blocks key needs attention,see **atServerKey Key Description**。 + +serviceApi: Contains one or more `serviceRoute` syntax blocks + +serviceRoute: Contains `atDoc`, handler and `route` in sequence mode + +atDoc: Optional syntax block, a key-value description of a route, which will be passed to the spec.Spec structure after parsing. If you don't care about passing it to spec.Spec, it is recommended to use a single-line comment instead. + +handler: It is a description of the handler layer of routing. You can specify the handler name by specifying the `handler` key by atServer, or you can directly use the atHandler syntax block to define the handler name + +atHandler: `@handler` fixed token, followed by a value following the regularity `[_a-zA-Z][a-zA-Z_-]`, used to declare a handler name + +route: Routing consists of `httpMethod`, `path`, optional `request`, optional `response`, and `httpMethod` must be lowercase. + +body: api request body grammar definition, it must be an optional ID value wrapped by () + +replyBody: api response body grammar definition, must be a struct wrapped by ()、~~array(Forward compatible processing, it may be discarded in the future, it is strongly recommended to wrap it in struct instead of using array directly as the response body)~~ + +kvLit: Same as info key-value + +serviceName: There can be multiple'-'join ID values + +path: The api request path must start with `/` or `/:`, and must not end with `/`. The middle can contain ID or multiple ID strings with `-` join + +**atServerKey Key Description** + +When modifying the service + + + + + + + + + + + + + + +
keyDescriptionExample
jwtDeclare that all routes under the current service require jwt authentication, and code containing jwt logic will be automatically generatedjwt: Auth
groupDeclare the current service or routing file groupgroup: login
middlewareDeclare that the current service needs to open the middlewaremiddleware: AuthMiddleware
+ +When modifying the route + + + + + + + + +
keyDescriptionExample
handlerDeclare a handler-
+ +**Examples of correct syntax** ✅ + +eg1:Irregular writing + +```api +@server( + jwt: Auth + group: foo + middleware: AuthMiddleware +) +service foo-api{ + @doc( + summary: foo + ) + @server( + handler: foo + ) + // Non-exported body + post /foo/:id (foo) returns (bar) + + @doc "bar" + @handler bar + post /bar returns ([]int)// Array is not recommended as response body + + @handler fooBar + post /foo/bar (Foo) returns // You can omit 'returns' +} +``` + +eg2:Standard writing (recommended) + +```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) +} + +``` + +**Examples of incorrect syntax** ❌ + +```api +// Empty server syntax block is not supported +@server( +) +// Empty service syntax block is not supported +service foo-api{ +} + +service foo-api{ + @doc kkkk // The short version of the doc must be enclosed in English double quotation marks + @handler foo + post /foo + + @handler foo // Duplicate handler + post /bar + + @handler fooBar + post /bar // Duplicate routing + + // @handler and @doc are in the wrong order + @handler someHandler + @doc "some doc" + post /some/path + + // handler is missing + post /some/path/:id + + @handler reqTest + post /foo/req (*Foo) // Data types other than ordinary structures are not supported as the request body + + @handler replyTest + post /foo/reply returns (*Foo) // Does not support data types other than ordinary structures and arrays (forward compatibility, later considered to be discarded) as response bodies +} +``` + +## Hidden channel + +Hidden channels are currently mainly blank symbols, newline symbols and comments. Here we only talk about comments, because blank symbols and newline symbols are currently useless. + +### Single line comment + +**Grammar definition** + +```antlrv4 +'//' ~[\r\n]* +``` + +**Grammar description** +It can be known from the grammatical definition that single-line comments must start with `//`, and the content cannot contain newline characters + +**Examples of correct syntax** ✅ + +```api +// doc +// comment +``` + +**Examples of incorrect syntax** ❌ + +```api +// break +line comments +``` + +### java style documentation comments + +**Grammar definition** + +```antlrv4 +'/*' .*? '*/' +``` + +**Grammar description** + +It can be known from the grammar definition that a single line comment must start with any character that starts with `/*` and ends with `*/`. + +**Examples of correct syntax** ✅ + +```api +/** + * java-style doc + */ +``` + +**Examples of incorrect syntax** ❌ + +```api +/* + * java-style doc */ + */ +``` + +## Doc&Comment + +If you want to get the doc or comment of a certain element, how do you define it? + +**Doc** + +We stipulate that the number of lines in the previous grammar block (non-hidden channel content) +line+1 to all comments (the current line, or multiple lines) before the first element of the current grammar block are doc, +And retain the original mark of `//`, `/*`, `*/`. + +**Comment** + +We specify that a comment block (the current line, or multiple lines) at the beginning of the line where the last element of the current syntax block is located is comment, +And retain the original mark of `//`, `/*`, `*/`. + +Syntax block **Doc** and **Comment** support situation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Syntax blockParent Syntax BlockDocComment
syntaxLitapi
kvLitinfoSpec
importLitimportSpec
typeLitapi
typeLittypeBlock
fieldtypeLit
key-valueatServer
atHandlerserviceRoute
routeserviceRoute
+ +The following is the writing of doc and comment after the corresponding syntax block is parsed + +```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 + * Enable jwt authentication + */ + jwt: Auth /**kvLit comment*/ +) +service foo-api{ + // atHandler doc + @handler foo //atHandler comment + + /* + * Route doc + * Post request + * Route path: foo + * Request body: Foo + * Response body: Foo + */ + post /foo (Foo) returns (Foo) // route comment +} +``` diff --git a/go-zero.dev/en/bloom.md b/go-zero.dev/en/bloom.md new file mode 100644 index 00000000..e7b17a2d --- /dev/null +++ b/go-zero.dev/en/bloom.md @@ -0,0 +1,91 @@ +# bloom + +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +The go-zero microservice framework provides many out-of-the-box tools. +Good tools can not only improve the performance of the service, +but also improve the robustness of the code to avoid errors, +and realize the uniformity of the code style for others to read, etc. +A series of articles will respectively introduce the use of tools in the go-zero framework and their implementation principles. + +## Bloom filter [bloom](https://github.com/zeromicro/go-zero/blob/master/core/bloom/bloom.go) +When doing server development, I believe you have heard of Bloom filters, +you can judge whether a certain element is in the collection, +because there are certain misjudgments and delete complex problems, +the general usage scenario is: to prevent cache breakdown (to prevent malicious Attacks), +spam filtering, cache digests, model detectors, etc., +to determine whether there is a row of data to reduce disk access and improve service access performance. +The simple cache package bloom.bloom provided by go-zero, the simple way to use it is as follows. + +```go +// Initialize redisBitSet +store := redis.NewRedis("redis 地址", redis.NodeType) +// Declare a bitSet, key="test_key" name and bits are 1024 bits +bitSet := newRedisBitSet(store, "test_key", 1024) +// Determine whether the 0th bit exists +isSetBefore, err := bitSet.check([]uint{0}) + +// Set the 512th bit to 1 +err = bitSet.set([]uint{512}) +// Expires in 3600 seconds +err = bitSet.expire(3600) + +// Delete the bitSet +err = bitSet.del() +``` + + +Bloom briefly introduced the use of the most basic redis bitset. The following is the real bloom implementation. + +Position the element hash + +```go +// The element is hashed 14 times (const maps=14), and byte (0-13) is appended to the element each time, and then the hash is performed. +// Take the modulo of locations[0-13], and finally return to 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 +} +``` + + +Add elements to bloom +```go +// We can find that the add method uses the set methods of getLocations and bitSet. +// We hash the elements into uint slices of length 14, and then perform the set operation and store them in the bitSet of redis. +func (f *BloomFilter) Add(data []byte) error { + locations := f.getLocations(data) + err := f.bitSet.set(locations) + if err != nil { + return err + } + return nil +} +``` + + +Check if there is an element in bloom +```go +// We can find that the Exists method uses the check method of getLocations and bitSet +// We hash the elements into uint slices of length 14, and then perform bitSet check verification, return true if it exists, false if it does not exist or if the check fails +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 +} +``` + +This section mainly introduces the `core.bloom` tool in the go-zero framework, which is very practical in actual projects. Good use of tools is very helpful to improve service performance and development efficiency. I hope this article can bring you some gains. \ No newline at end of file diff --git a/go-zero.dev/en/buiness-cache.md b/go-zero.dev/en/buiness-cache.md new file mode 100644 index 00000000..19a239db --- /dev/null +++ b/go-zero.dev/en/buiness-cache.md @@ -0,0 +1,152 @@ +# Business layer cache + +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +In the previous article [Persistent Layer Cache](redis-cache.md), the db layer cache was introduced. In retrospect, the main design of the db layer cache can be summarized as follows: + +* The cache is only deleted but not updated +* Only one row record is always stored, that is, the row record corresponding to the primary key +* The unique index only caches the primary key value, not the row record directly (refer to the mysql index idea) +* Anti-cache penetration design, one minute by default +* Do not cache multi-line records + +## Preface + +In a large-scale business system, by adding a cache to the persistence layer, for most single-line record queries, +it is believed that the cache can help the persistence layer reduce a lot of access pressure, but in actual business, +data reading is not just a single-line record. +In the face of many multi-line records, this will also cause a lot of access pressure on the persistence layer. +In addition, it is unrealistic to rely solely on the persistence layer for high-concurrency scenarios such as the spike system and the course selection system. +In this section, we introduce the cache design in go-zero practice-biz cache. + +## Examples of applicable scenarios + +* subject system +* Content social system +* Spike... + +Like these systems, we can add another layer of cache to the business layer to store key information in the system, +such as the student selection information in the course selection system, the remaining number of courses in the course selection system, +and the content information during a certain period of time in the content social system. + +Next, let's take an example of a content social system. + +In the content social system, we generally query a batch of content lists first, +and then click on a piece of content to view the details. + +Before adding biz cache, the query flowchart of content information should be: + +![redis-cache-05](./resource/redis-cache-05.png) + +From the figure and the previous article [Persistence Layer Cache] (redis-cache.md), +we can know that there is no way to get the content list to rely on the cache. +If we add a layer of cache to the business layer to store the key information (or even the complete information) in the list, +then access to multiple rows of records is no longer a problem, and this is what biz redis will do. Next, +let’s take a look at the design plan, assuming that a single-line record in the content system contains the following fields. + +|Field Name|Field Type|Remarks| +|---|---|---| +|id|string|Content id| +|title|string|Title| +|content|string|Content| +|createTime|time.Time|Create time| + +Our goal is to obtain a batch of content lists, and try to avoid the access pressure caused by the content list going to the db. +First, we use the sort set data structure of redis to store. The amount of field information that needs to be stored is based on +two redis storage schemes: + +* Cache local information + + ![biz-redis-02](./resource/biz-redis-02.svg) + The key field information (such as id, etc.) is compressed and stored according to certain rules. + For score, we use the `createTime` millisecond value (the time value is equal, not discussed here). + The advantage of this storage scheme is to save redis storage space. + + On the other hand, the disadvantage is that the detailed content of the list needs to be checked back again (but this back check will use the row record cache of the persistence layer) + +* Cache complete information + + ![biz-redis-01](./resource/biz-redis-01.svg) + All published content will be stored after being compressed according to certain rules. For the same score, + we still use the `createTime` millisecond value. The advantage of this storage solution is that business additions, + deletions, checks, and changes are all reids, while the db layer is at this time. + + You don’t need to consider the row record cache. The persistence layer only provides data backup and recovery. + On the other hand, its shortcomings are also obvious. The storage space and configuration requirements are higher, and the cost will increase. + +Sample code +```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 provides content storage +func AddContent(r redis.Redis, c *Content) error { + v := compress(c) + _, err := r.Zadd(bizContentCacheKey, c.CreateTime.UnixNano()/1e6, v) + return err +} + +// DelContent provides content deletion +func DelContent(r redis.Redis, c *Content) error { + v := compress(c) + _, err := r.Zrem(bizContentCacheKey, v) + + return err +} + +// Content compression +func compress(c *Content) string { + // todo: do it yourself + var ret string + return ret +} + +// Content decompression +func unCompress(v string) *Content { + // todo: do it yourself + var ret Content + return &ret +} + +// ListByRangeTime provides data query based on time period +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 +} + +``` + +In the above example, redis does not set an expiration time. We will synchronize the add, delete, modify, +and check operations to redis. We think that the content social system has a relatively high list access request to do this scheme design. +In addition, there are also some data visits. I did not expect the content design system to visit so frequently. +It may be a sudden increase in visits within a certain period of time, and then it may be visited again for a long time. At this interval, +In other words, I will not visit again. In this scenario, how should I consider the design of the cache? +In the practice of go-zero content, there are two solutions to this problem: + +* Increased memory cache: The memory cache is used to store data that may have a large amount of sudden access. Commonly used storage schemes use map data structure to store. + Map data storage is relatively simple to implement, but cache expiration processing needs to be increased + The timer comes out, another solution is through [Cache](https://github.com/zeromicro/go-zero/blob/master/core/collection/cache.go) in the go-zero library, It is specialized + Used for memory management. +* Use biz redis and set a reasonable expiration time + +# Summary +The above two scenarios can contain most of the multi-line record cache. For scenarios where the query volume of multi-line records is not large, +there is no need to put biz redis directly in it. You can try to let db take care of it first, and developers can monitor according to the persistence layer. And service +Biz needs to be introduced when monitoring to measure. diff --git a/go-zero.dev/en/business-coding.md b/go-zero.dev/en/business-coding.md new file mode 100644 index 00000000..433e8700 --- /dev/null +++ b/go-zero.dev/en/business-coding.md @@ -0,0 +1,128 @@ +# Business code + +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +In the previous section, we have written user.api based on the preliminary requirements to describe which services the user service provides to the outside world. In this section, we will continue with the previous steps. +Use business coding to tell how go-zero is used in actual business. + +## Add Mysql configuration +```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 +} +``` + +## Improve yaml configuration +```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 database user +> +> $password: mysql database password +> +> $url: mysql database connection address +> +> $db: mysql database db name, that is, the database where the user table is located +> +> $host: Redis connection address Format: ip:port, such as: 127.0.0.1:6379 +> +> $pass: redis password +> +> For more configuration information, please refer to [api configuration introduction](api-config.md) + +## Improve service dependence +```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), + } +} +``` +## Fill in the login logic +```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("Invalid parameter") + } + + userInfo, err := l.svcCtx.UserModel.FindOneByNumber(req.Username) + switch err { + case nil: + case model.ErrNotFound: + return nil, errors.New("Username does not exist") + default: + return nil, err + } + + if userInfo.Password != req.Password { + return nil, errors.New("User password is incorrect") + } + + // ---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] +> For the code implementation of [start]-[end] in the above code, please refer to the [Jwt Authentication](jwt.md) chapter + +# Guess you wants +* [API IDL](api-grammar.md) +* [API Commands](goctl-api.md) +* [API Directory Structure](api-dir.md) +* [JWT](jwt.md) +* [API Configuration](api-config.md) \ No newline at end of file diff --git a/go-zero.dev/en/business-dev.md b/go-zero.dev/en/business-dev.md new file mode 100644 index 00000000..a2c31168 --- /dev/null +++ b/go-zero.dev/en/business-dev.md @@ -0,0 +1,63 @@ +# Business development + +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +In this chapter, we use a simple example to demonstrate some basic functions in go-zero. This section will contain the following subsections: + * [Directory Structure](service-design.md) + * [Model Generation](model-gen.md) + * [API Coding](api-coding.md) + * [Business Coding](business-coding.md) + * [JWT](jwt.md) + * [Middleware](middleware.md) + * [RPC Implement & Call](rpc-call.md) + * [Error Handling](error-handle.md) + +## Demo project download +Before officially entering the follow-up document description, you can pay attention to the source code here, and we will perform a progressive demonstration of the function based on this source code. +Instead of starting from 0 completely, if you come from the [Quick Start](quick-start.md) chapter, this source code structure is not a problem for you. + +
Click Here to download Demo project + +## Demonstration project description + +### Scenes +The programmer Xiao Ming needs to borrow a copy of "Journey to the West". When there is no online library management system, he goes to the front desk of the library to consult with the librarian every day. +* Xiao Ming: Hello, do you still have the book "Journey to the West" today? +* Administrator: No more, let's check again tomorrow. + +One day later, Xiao Ming came to the library again and asked: +* Xiao Ming: Hello, do you still have the book "Journey to the West" today? +* Administrator: No, you can check again in two days. + +After many repetitions in this way, Xiao Ming was also in vain and wasted a lot of time on the way back and forth, so he finally couldn't stand the backward library management system. +He decided to build a book review system by himself. + +### Expected achievement +* User login: + Rely on existing student system data to log in +* Book search: + Search for books based on book keywords and query the remaining number of books. + +### System analysis + +#### Service design +* user + * api: provides user login protocol + * rpc: for search service to access user data +* search + * api: provide book query agreement + +> [!TIP] +> Although this tiny book borrowing query system is small, it does not fit the business scenario in practice, but only the above two functions have already met our demonstration of the go-zero api/rpc scenario. +> In order to satisfy the richer go-zero function demonstration in the future, business insertion, that is, related function descriptions, will be carried out in the document. Here only one scene is used for introduction. +> +> NOTE: Please create the sql statement in the user into the db by yourself, see [prepare](prepare.md) for more preparation work +> +> Add some preset user data to the database for later use. For the sake of space, the demonstration project does not demonstrate the operation of inserting data in detail. + + +# Reference preset data +```sql +INSERT INTO `user` (number,name,password,gender)values ('666','xiaoming','123456','male'); +``` \ No newline at end of file diff --git a/go-zero.dev/en/ci-cd.md b/go-zero.dev/en/ci-cd.md new file mode 100644 index 00000000..bb11ea50 --- /dev/null +++ b/go-zero.dev/en/ci-cd.md @@ -0,0 +1,60 @@ +# CI/CD +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + + +> In software engineering, CI/CD or CICD generally refers to the combined practices of continuous integration and either continuous delivery or continuous deployment. +> +> ——[Wikipedia](https://zh.wikipedia.org/wiki/CI/CD) + + +![cd-cd](./resource/ci-cd.png) + +## What can CI do? + +> In modern application development, the goal is to have multiple developers working simultaneously on different features of the same app. However, if an organization is set up to merge all branching source code together on one day (known as “merge day”), the resulting work can be tedious, manual, and time-intensive. That’s because when a developer working in isolation makes a change to an application, there’s a chance it will conflict with different changes being simultaneously made by other developers. This problem can be further compounded if each developer has customized their own local integrated development environment (IDE), rather than the team agreeing on one cloud-based IDE. + +> ——[Continuous integration](https://www.redhat.com/en/topics/devops/what-is-ci-cd) + +From a conceptual point of view, CI/CD includes the deployment process. Here, we will put the deployment (CD) in a separate section [Service Deployment](service-deployment.md), +This section uses gitlab to do a simple CI (Run Unit Test) demonstration. + +## Gitlab CI +Gitlab CI/CD is a built-in software development tool of Gitlab, providing +* Continuous Integration (CI) +* Continuous Delivery (CD) +* Continuous deployment (CD) + +## Prepare +* gitlab installation +* git installation +* gitlab runner installation + +## Enable Gitlab CI +* Upload code + * Create a new warehouse `go-zero-demo` in gitlab + * Upload the local code to the `go-zero-demo` warehouse +* Create a `.gitlab-ci.yaml` file in the project root directory. Through this file, a pipeline can be created, which will be run when there is a content change in the code repository. The pipeline is run in sequence by one or more. + Each stage can contain one or more jobs running in parallel. +* Add CI content (for reference only) + + ```yaml + stages: + - analysis + + analysis: + stage: analysis + image: golang + script: + - go version && go env + - go test -short $(go list ./...) | grep -v "no test" + ``` + +> [!TIP] +> The above CI is a simple demonstration. For detailed gitlab CI, please refer to the official gitlab documentation for richer CI integration. + + +# Reference +* [CI/CD Wikipedia](https://zh.wikipedia.org/wiki/CI/CD) +* [Continuous integration](https://www.redhat.com/en/topics/devops/what-is-ci-cd) +* [Gitlab CI](https://docs.gitlab.com/ee/ci/) \ No newline at end of file diff --git a/go-zero.dev/en/coding-spec.md b/go-zero.dev/en/coding-spec.md new file mode 100644 index 00000000..d4983cf6 --- /dev/null +++ b/go-zero.dev/en/coding-spec.md @@ -0,0 +1,46 @@ +# Coding Rules + +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +## import +* Single-line import is not recommended being wrapped in parentheses +* Introduce in the order of `Official Package`, NEW LINE, `Project Package`, NEW LINE, `Third Party Dependent Package` + ```go + import ( + "context" + "string" + + "greet/user/internal/config" + + "google.golang.org/grpc" + ) + ``` + +## Function returns +* Object avoids non-pointer return +* Follow the principle that if there is a normal value return, there must be no error, and if there is an error, there must be no normal value return. + +## Error handling +* An error must be handled, if it cannot be handled, it must be thrown. +* Avoid underscore (_) receiving error + +## Function body coding +* It is recommended that a block end with a blank line, such as if, for, etc. + ```go + func main (){ + if x==1{ + // do something + } + + fmt.println("xxx") + } + ``` +* Blank line before return + ```go + func getUser(id string)(string,error){ + .... + + return "xx",nil + } + ``` \ No newline at end of file diff --git a/go-zero.dev/en/concept-introduction.md b/go-zero.dev/en/concept-introduction.md new file mode 100644 index 00000000..91ee8ddd --- /dev/null +++ b/go-zero.dev/en/concept-introduction.md @@ -0,0 +1,46 @@ +# Concepts + +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + + +## go-zero +go-zero is a web and rpc framework that with lots of engineering practices builtin. It’s born to ensure the stability of the busy services with resilience design, and has been serving sites with tens of millions users for years. + +## goctl +An auxiliary tool designed to improve engineering efficiency and reduce error rates for developers. + +## goctl plugins +Refers to the peripheral binary resources centered on goctl, which can meet some personalized code generation requirements, such as the routing merge plug-in `goctl-go-compact` plug-in, +The `goctl-swagger` plugin for generating swagger documents, the `goctl-php` plugin for generating the php caller, etc. + +## intellij/vscode plugins +A plug-in developed with goctl on the intellij series products, which replaces the goctl command line operation with the UI. + +## api file +An api file refers to a text file used to define and describe an api service. It ends with the .api suffix and contains IDL of the api syntax. + +## goctl environment +The goctl environment is the preparation environment before using goctl, including: +* golang environment +* protoc +* protoc-gen-go plugin +* go module | gopath + +## go-zero-demo +Go-zero-demo contains a large repository of all the source code in the document. When we write the demo in the future, we all create sub-projects under this project. +Therefore, we need to create a large warehouse in advance `go-zero-demo`, and I put this warehouse in the home directory here. + +```shell +$ cd ~ +$ mkdir go-zero-demo&&cd go-zero-demo +$ go mod init go-zero-demo +``` + + +# Reference +* [go-zero](README.md) +* [Goctl](goctl.md) +* [Plugins](plugin-center.md) +* [Tools](tool-center.md) +* [API IDL](api-grammar.md) \ No newline at end of file diff --git a/go-zero.dev/en/config-introduction.md b/go-zero.dev/en/config-introduction.md new file mode 100644 index 00000000..87eca118 --- /dev/null +++ b/go-zero.dev/en/config-introduction.md @@ -0,0 +1,8 @@ +# Configuration Introduction + +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +Before officially using go-zero, let us first understand the configuration definitions of different service types in go-zero, and see what role each field in the configuration has. This section will contain the following subsections: +* [API Configuration](api-config.md) +* [RPC Configuration](rpc-config.md) \ No newline at end of file diff --git a/go-zero.dev/en/datacenter.md b/go-zero.dev/en/datacenter.md new file mode 100644 index 00000000..78b080ea --- /dev/null +++ b/go-zero.dev/en/datacenter.md @@ -0,0 +1,918 @@ +# How do I use go-zero to implement a Middle Ground System? + +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + + +> Author: Jack Luo +> +> Original link:https://www.cnblogs.com/jackluo/p/14148518.html + +[TOC] + +I recently discovered that a new star microservice framework has emerged in the golang community. +It comes from a good future. Just looking at the name, it is very exciting. Before, I only played go-micro. +In fact, I have not used it in the project. I think that microservices and grpc are very noble. +They have not been in the project yet. I have really played them. I saw that the tools provided by the government are really easy to use. +They only need to be defined, and the comfortable file structure is generated. I am concerned about business, and there was a voting activity recently, +and in recent years, Middle Ground System have been quite popular, so I decided to give it a try. + +> SourceCode: [https://github.com/jackluo2012/datacenter](https://github.com/jackluo2012/datacenter) + +Let's talk about the idea of Middle Ground System architecture first: + +![](https://img2020.cnblogs.com/blog/203395/202012/203395-20201217094615171-335437652.jpg) + +The concept of Middle Ground System is probably to unify the apps one by one. I understand it this way anyway. + +Let’s talk about user service first. Now a company has a lot of official accounts, small programs, WeChat, Alipay, and xxx xxx, and a lot of platforms. Every time we develop, we always need to provide user login services. Stop copying the code, and then we are thinking about whether we can have a set of independent user services, just tell me you need to send a platform you want to log in (such as WeChat), WeChat login, what is needed is that the client returns one to the server code, and then the server takes this code to WeChat to get user information. Anyway, everyone understands it. + +We decided to get all the information into the configuration public service, and then store the appid and appkey of WeChat, Alipay and other platforms, as well as the appid and appkey of payment, and write a set. + +--- + +Finally, let's talk about implementation, the whole is a repo: + +- Gateway, we use: go-zero's Api service +- Others are services, we use go-zero rpc service + +Look at the directory structure + +![](https://img2020.cnblogs.com/blog/203395/202012/203395-20201209110504600-317546535.png) + +After the whole project was completed, I worked alone and wrote about it for a week, and then I realized the above Middle Ground System. + +## datacenter-api service + + +Look at the official document first [https://zeromicro.github.io/go-zero/](https://zeromicro.github.io/go-zero/) + +Let's set up the gateway first:: + +```shell +➜ blogs mkdir datacenter && cd datacenter +➜ datacenter go mod init datacenter +go: creating new go.mod: module datacenter +➜ datacenter +``` + +View the book directory: + + +``` +➜ datacenter tree +. +└── go.mod + +0 directories, 1 file +``` + + +### Create api file + + +``` +➜ datacenter goctl api -o datacenter.api +Done. +➜ datacenter tree +. +├── datacenter.api +└── go.mod +``` + + +### Define api service + + +Respectively include the above **Public Service**, **User Service**, **Voting Activity Service** + + +``` +info( + title: "demo" + desc: "demo" + author: "jackluo" + email: "net.webjoy@gmail.com" +) + +// Get application information +type Beid struct { + Beid int64 `json:"beid"` +} +type Token struct{ + Token string `json:"token"` +} +type WxTicket struct{ + Ticket string `json:"ticket"` +} +type Application struct { + Sname string `json:"Sname"` + Logo string `json:"logo"` + Isclose int64 `json:"isclose"` + Fullwebsite string `json:"fullwebsite"` +} +type SnsReq struct{ + Beid + Ptyid int64 `json:"ptyid"` // Platform ID + BackUrl string `json:"back_url"` // Return address after login +} +type SnsResp struct{ + Beid + Ptyid int64 `json:"ptyid"` // Platform ID + Appid string `json:"appid"` // sns Platform ID + Title string `json:"title"` + LoginUrl string `json:"login_url"` // WeChat login address +} + +type WxShareResp struct { + Appid string `json:"appid"` + Timestamp int64 `json:"timestamp"` + Noncestr string `json:"noncestr"` + Signature string `json:"signature"` +} + +@server( + group: common +) +service datacenter-api { + @doc( + summary: "Get information about the site" + ) + @handler votesVerification + get /MP_verify_NT04cqknJe0em3mT.txt (SnsReq) returns (SnsResp) + + @handler appInfo + get /common/appinfo (Beid) returns (Application) + + @doc( + summary: "Get social attribute information of the site" + ) + @handler snsInfo + post /common/snsinfo (SnsReq) returns (SnsResp) + // Get shared returns + @handler wxTicket + post /common/wx/ticket (SnsReq) returns (WxShareResp) + +} + +@server( + jwt: Auth + group: common +) +service datacenter-api { + @doc( + summary: "Qiniu upload credentials" + ) + @handler qiuniuToken + post /common/qiuniu/token (Beid) returns (Token) +} + +// Registration request +type RegisterReq struct { + Mobile string `json:"mobile"` + Password string `json:"password"` + Smscode string `json:"smscode"` +} +// Login request +type LoginReq struct{ + Mobile string `json:"mobile"` + Type int64 `json:"type"` // 1. Password login, 2. SMS login + Password string `json:"password"` +} +// WeChat login +type WxLoginReq struct { + Beid int64 `json:"beid"` // Application id + Code string `json:"code"` // WeChat AccesskKey + Ptyid int64 `json:"ptyid"` // Platform ID +} + +//Return user information +type UserReply struct { + Auid int64 `json:"auid"` + Uid int64 `json:"uid"` + Beid int64 `json:"beid"` // Platform ID + Ptyid int64 `json:"ptyid"` + Username string `json:"username"` + Mobile string `json:"mobile"` + Nickname string `json:"nickname"` + Openid string `json:"openid"` + Avator string `json:"avator"` + JwtToken +} + +type AppUser struct{ + Uid int64 `json:"uid"` + Auid int64 `json:"auid"` + Beid int64 `json:"beid"` + Ptyid int64 `json:"ptyid"` + Nickname string `json:"nickname"` + Openid string `json:"openid"` + Avator string `json:"avator"` +} + +type LoginAppUser struct{ + Uid int64 `json:"uid"` + Auid int64 `json:"auid"` + Beid int64 `json:"beid"` + Ptyid int64 `json:"ptyid"` + Nickname string `json:"nickname"` + Openid string `json:"openid"` + Avator string `json:"avator"` + JwtToken +} + +type JwtToken struct { + AccessToken string `json:"access_token,omitempty"` + AccessExpire int64 `json:"access_expire,omitempty"` + RefreshAfter int64 `json:"refresh_after,omitempty"` +} + +type UserReq struct{ + Auid int64 `json:"auid"` + Uid int64 `json:"uid"` + Beid int64 `json:"beid"` + Ptyid int64 `json:"ptyid"` +} + +type Request { + Name string `path:"name,options=you|me"` +} +type Response { + Message string `json:"message"` +} + +@server( + group: user +) +service user-api { + @handler ping + post /user/ping () + + @handler register + post /user/register (RegisterReq) returns (UserReply) + + @handler login + post /user/login (LoginReq) returns (UserReply) + + @handler wxlogin + post /user/wx/login (WxLoginReq) returns (LoginAppUser) + + @handler code2Session + get /user/wx/login () returns (LoginAppUser) +} +@server( + jwt: Auth + group: user + middleware: Usercheck +) +service user-api { + @handler userInfo + get /user/dc/info (UserReq) returns (UserReply) +} + + +type Actid struct { + Actid int64 `json:"actid"` +} + +type VoteReq struct { + Aeid int64 `json:"aeid"` + Actid +} +type VoteResp struct { + VoteReq + Votecount int64 `json:"votecount"` + Viewcount int64 `json:"viewcount"` +} + + +type ActivityResp struct { + Actid int64 `json:"actid"` + Title string `json:"title"` + Descr string `json:"descr"` + StartDate int64 `json:"start_date"` + EnrollDate int64 `json:"enroll_date"` + EndDate int64 `json:"end_date"` + Votecount int64 `json:"votecount"` + Viewcount int64 `json:"viewcount"` + Type int64 `json:"type"` + Num int64 `json:"num"` +} + +type EnrollReq struct { + Actid + Name string `json:"name"` + Address string `json:"address"` + Images []string `json:"images"` + Descr string `json:"descr"` +} + +type EnrollResp struct { + Actid + Aeid int64 `json:"aeid"` + Name string `json:"name"` + Address string `json:"address"` + Images []string `json:"images"` + Descr string `json:"descr"` + Votecount int64 `json:"votecount"` + Viewcount int64 `json:"viewcount"` + +} + +@server( + group: votes +) +service votes-api { + @doc( + summary: "Get activity information" + ) + @handler activityInfo + get /votes/activity/info (Actid) returns (ActivityResp) + @doc( + summary: "Activity visit +1" + ) + @handler activityIcrView + get /votes/activity/view (Actid) returns (ActivityResp) + @doc( + summary: "Get information about registered voting works" + ) + @handler enrollInfo + get /votes/enroll/info (VoteReq) returns (EnrollResp) + @doc( + summary: "Get a list of registered works" + ) + @handler enrollLists + get /votes/enroll/lists (Actid) returns(EnrollResp) +} + +@server( + jwt: Auth + group: votes + middleware: Usercheck +) +service votes-api { + @doc( + summary: "vote" + ) + @handler vote + post /votes/vote (VoteReq) returns (VoteResp) + @handler enroll + post /votes/enroll (EnrollReq) returns (EnrollResp) +} +``` + + +The API and document ideas that are basically written above + + +### Generate datacenter api service + + +``` +➜ datacenter goctl api go -api datacenter.api -dir . +Done. +➜ datacenter tree +. +├── datacenter.api +├── etc +│ └── datacenter-api.yaml +├── go.mod +├── internal +│ ├── config +│ │ └── config.go +│ ├── handler +│ │ ├── common +│ │ │ ├── appinfohandler.go +│ │ │ ├── qiuniutokenhandler.go +│ │ │ ├── snsinfohandler.go +│ │ │ ├── votesverificationhandler.go +│ │ │ └── wxtickethandler.go +│ │ ├── routes.go +│ │ ├── user +│ │ │ ├── code2sessionhandler.go +│ │ │ ├── loginhandler.go +│ │ │ ├── pinghandler.go +│ │ │ ├── registerhandler.go +│ │ │ ├── userinfohandler.go +│ │ │ └── wxloginhandler.go +│ │ └── votes +│ │ ├── activityicrviewhandler.go +│ │ ├── activityinfohandler.go +│ │ ├── enrollhandler.go +│ │ ├── enrollinfohandler.go +│ │ ├── enrolllistshandler.go +│ │ └── votehandler.go +│ ├── logic +│ │ ├── common +│ │ │ ├── appinfologic.go +│ │ │ ├── qiuniutokenlogic.go +│ │ │ ├── snsinfologic.go +│ │ │ ├── votesverificationlogic.go +│ │ │ └── wxticketlogic.go +│ │ ├── user +│ │ │ ├── code2sessionlogic.go +│ │ │ ├── loginlogic.go +│ │ │ ├── pinglogic.go +│ │ │ ├── registerlogic.go +│ │ │ ├── userinfologic.go +│ │ │ └── wxloginlogic.go +│ │ └── votes +│ │ ├── activityicrviewlogic.go +│ │ ├── activityinfologic.go +│ │ ├── enrollinfologic.go +│ │ ├── enrolllistslogic.go +│ │ ├── enrolllogic.go +│ │ └── votelogic.go +│ ├── middleware +│ │ └── usercheckmiddleware.go +│ ├── svc +│ │ └── servicecontext.go +│ └── types +│ └── types.go +└── datacenter.go + +14 directories, 43 files +``` + + +We open `etc/datacenter-api.yaml` and add the necessary configuration information + + +```yaml +Name: datacenter-api +Log: + Mode: console +Host: 0.0.0.0 +Port: 8857 +Auth: + AccessSecret: secret + AccessExpire: 86400 +CacheRedis: +- Host: 127.0.0.1:6379 + Pass: pass + Type: node +UserRpc: + Etcd: + Hosts: + - 127.0.0.1:2379 + Key: user.rpc +CommonRpc: + Etcd: + Hosts: + - 127.0.0.1:2379 + Key: common.rpc +VotesRpc: + Etcd: + Hosts: + - 127.0.0.1:2379 + Key: votes.rpc +``` + + +I will write the above `UserRpc`, `CommonRpc`, and `VotesRpc` first, and then add them by step. + + +Let's write the `CommonRpc` service first. + + +## CommonRpc service + + +### New project directory + + +``` +➜ datacenter mkdir -p common/rpc && cd common/rpc +``` + + +Just create it directly in the datacenter directory, because in common, it may not only provide rpc services in the future, but also api services, so the rpc directory is added + + +### goctl create template + + +``` +➜ rpc goctl rpc template -o=common.proto +➜ rpc ls +common.proto +``` + + +Fill in the content: + + +```protobufbuf +➜ rpc cat common.proto +syntax = "proto3"; + +package common; + +option go_package = "common"; + +message BaseAppReq{ + int64 beid=1; +} + +message BaseAppResp{ + int64 beid=1; + string logo=2; + string sname=3; + int64 isclose=4; + string fullwebsite=5; +} + +message AppConfigReq { + int64 beid=1; + int64 ptyid=2; +} + +message AppConfigResp { + int64 id=1; + int64 beid=2; + int64 ptyid=3; + string appid=4; + string appsecret=5; + string title=6; +} + +service Common { + rpc GetAppConfig(AppConfigReq) returns(AppConfigResp); + rpc GetBaseApp(BaseAppReq) returns(BaseAppResp); +} +``` + + +### gotcl generates rpc service + + +```bash +➜ rpc goctl rpc proto -src common.proto -dir . +protoc -I=/Users/jackluo/works/blogs/datacenter/common/rpc common.proto --go_out=plugins=grpc:/Users/jackluo/works/blogs/datacenter/common/rpc/common +Done. +``` + + +``` +➜ rpc tree +. +├── common +│ └── common.pb.go +├── common.go +├── common.proto +├── commonclient +│ └── common.go +├── etc +│ └── common.yaml +└── internal +├── config +│ └── config.go +├── logic +│ ├── getappconfiglogic.go +│ └── getbaseapplogic.go +├── server +│ └── commonserver.go +└── svc +└── servicecontext.go + +8 directories, 10 files +``` + + +Basically, all the catalog specifications and structure are generated, so there is no need to worry about the project catalog, how to put it, and how to organize it. + + +Take a look at the configuration information, which can write mysql and other redis information: + + +```yaml +Name: common.rpc +ListenOn: 127.0.0.1:8081 +Mysql: + DataSource: root:admin@tcp(127.0.0.1:3306)/datacenter?charset=utf8&parseTime=true&loc=Asia%2FShanghai +CacheRedis: +- Host: 127.0.0.1:6379 + Pass: + Type: node +Etcd: + Hosts: + - 127.0.0.1:2379 + Key: common.rpc +``` + + +Let's add database services: + + +``` +➜ rpc cd .. +➜ common ls +rpc +➜ common pwd +/Users/jackluo/works/blogs/datacenter/common +➜ common goctl model mysql datasource -url="root:admin@tcp(127.0.0.1:3306)/datacenter" -table="base_app" -dir ./model -c +Done. +➜ common tree +. +├── model +│ ├── baseappmodel.go +│ └── vars.go +└── rpc + ├── common + │ └── common.pb.go + ├── common.go + ├── common.proto + ├── commonclient + │ └── common.go + ├── etc + │ └── common.yaml + └── internal + ├── config + │ └── config.go + ├── logic + │ ├── getappconfiglogic.go + │ └── getbaseapplogic.go + ├── server + │ └── commonserver.go + └── svc + └── servicecontext.go + +10 directories, 12 files +``` + + +So the basic `rpc` is finished, and then we connect the rpc with the model and the api. This official document is already very detailed, here is just the code: + + +```go +➜ common cat rpc/internal/config/config.go +package config + +import ( + "github.com/tal-tech/go-zero/core/stores/cache" + "github.com/tal-tech/go-zero/zrpc" +) + +type Config struct { + zrpc.RpcServerConf + Mysql struct { + DataSource string + } + CacheRedis cache.ClusterConf +} +``` + + +Modify in svc: + + +```go +➜ common cat rpc/internal/svc/servicecontext.go +package svc + +import ( + "datacenter/common/model" + "datacenter/common/rpc/internal/config" + + "github.com/tal-tech/go-zero/core/stores/sqlx" +) + +type ServiceContext struct { + c config.Config + AppConfigModel model.AppConfigModel + BaseAppModel model.BaseAppModel +} + +func NewServiceContext(c config.Config) *ServiceContext { + conn := sqlx.NewMysql(c.Mysql.DataSource) + apm := model.NewAppConfigModel(conn, c.CacheRedis) + bam := model.NewBaseAppModel(conn, c.CacheRedis) + return &ServiceContext{ + c: c, + AppConfigModel: apm, + BaseAppModel: bam, + } +} +``` + + +The above code has already associated `rpc` with the `model` database, we will now associate `rpc` with `api`: + + +```go +➜ datacenter cat internal/config/config.go + +package config + +import ( + "github.com/tal-tech/go-zero/core/stores/cache" + "github.com/tal-tech/go-zero/rest" + "github.com/tal-tech/go-zero/zrpc" +) + +type Config struct { + rest.RestConf + + Auth struct { + AccessSecret string + AccessExpire int64 + } + UserRpc zrpc.RpcClientConf + CommonRpc zrpc.RpcClientConf + VotesRpc zrpc.RpcClientConf + + CacheRedis cache.ClusterConf +} +``` + + +Join the `svc` service: + + +```go +➜ datacenter cat internal/svc/servicecontext.go +package svc + +import ( + "context" + "datacenter/common/rpc/commonclient" + "datacenter/internal/config" + "datacenter/internal/middleware" + "datacenter/shared" + "datacenter/user/rpc/userclient" + "datacenter/votes/rpc/votesclient" + "fmt" + "net/http" + "time" + + "github.com/tal-tech/go-zero/core/logx" + "github.com/tal-tech/go-zero/core/stores/cache" + "github.com/tal-tech/go-zero/core/stores/redis" + "github.com/tal-tech/go-zero/core/syncx" + "github.com/tal-tech/go-zero/rest" + "github.com/tal-tech/go-zero/zrpc" + "google.golang.org/grpc" +) + +type ServiceContext struct { + Config config.Config + GreetMiddleware1 rest.Middleware + GreetMiddleware2 rest.Middleware + Usercheck rest.Middleware + UserRpc userclient.User //用户 + CommonRpc commonclient.Common + VotesRpc votesclient.Votes + Cache cache.Cache + RedisConn *redis.Redis +} + +func timeInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { + stime := time.Now() + err := invoker(ctx, method, req, reply, cc, opts...) + if err != nil { + return err + } + + fmt.Printf("timeout %s: %v\n", method, time.Now().Sub(stime)) + return nil +} +func NewServiceContext(c config.Config) *ServiceContext { + + ur := userclient.NewUser(zrpc.MustNewClient(c.UserRpc, zrpc.WithUnaryClientInterceptor(timeInterceptor))) + cr := commonclient.NewCommon(zrpc.MustNewClient(c.CommonRpc, zrpc.WithUnaryClientInterceptor(timeInterceptor))) + vr := votesclient.NewVotes(zrpc.MustNewClient(c.VotesRpc, zrpc.WithUnaryClientInterceptor(timeInterceptor))) + //缓存 + ca := cache.NewCache(c.CacheRedis, syncx.NewSharedCalls(), cache.NewCacheStat("dc"), shared.ErrNotFound) + rcon := redis.NewRedis(c.CacheRedis[0].Host, c.CacheRedis[0].Type, c.CacheRedis[0].Pass) + return &ServiceContext{ + Config: c, + GreetMiddleware1: greetMiddleware1, + GreetMiddleware2: greetMiddleware2, + Usercheck: middleware.NewUserCheckMiddleware().Handle, + UserRpc: ur, + CommonRpc: cr, + VotesRpc: vr, + Cache: ca, + RedisConn: rcon, + } +} +``` + + +Basically, we can call it in the file directory of `logic`: + + +```go +cat internal/logic/common/appinfologic.go + +package logic + +import ( + "context" + + "datacenter/internal/svc" + "datacenter/internal/types" + "datacenter/shared" + + "datacenter/common/model" + "datacenter/common/rpc/common" + + "github.com/tal-tech/go-zero/core/logx" +) + +type AppInfoLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAppInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) AppInfoLogic { + return AppInfoLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AppInfoLogic) AppInfo(req types.Beid) (appconfig *common.BaseAppResp, err error) { + + err = l.svcCtx.Cache.GetCache(model.GetcacheBaseAppIdPrefix(req.Beid), appconfig) + if err != nil && err == shared.ErrNotFound { + appconfig, err = l.svcCtx.CommonRpc.GetBaseApp(l.ctx, &common.BaseAppReq{ + Beid: req.Beid, + }) + if err != nil { + return + } + err = l.svcCtx.Cache.SetCache(model.GetcacheBaseAppIdPrefix(req.Beid), appconfig) + } + + return +} +``` + + +In this way, it is basically connected, and basically there is no need to change the others. `UserRPC` and `VotesRPC` are similar, so I won't write them here. + + +## Reviews + + +`go-zero` is really fragrant, because it has a `goctl` tool, which can automatically generate all the code structure, we will no longer worry about the directory structure, how to organize it, and there is no architectural ability for several years It’s not easy to implement. What are the norms, concurrency, circuit breaker, no use at all, test and filter other, concentrate on realizing the business, like microservices, but also service discovery, a series of things, don’t care, because `go-zero` has been implemented internally. + + +I have written code for more than 10 years. The php I have been using before, the more famous ones are laravel and thinkphp, which are basically modular. Realizations like microservices are really costly, but you use go-zero. , You develop as simple as tune api interface, other service discovery, those do not need to pay attention at all, only need to pay attention to the business. + + +A good language, framework, and their underlying thinking are always high-efficiency and no overtime thinking. I believe that go-zero will improve the efficiency of you and your team or company. The author of go-zero said that they have a team dedicated to organizing the go-zero framework, and the purpose should be obvious, that is, to improve their own development efficiency, process flow, and standardization, which are the criteria for improving work efficiency, as we usually encounter When it comes to a problem or encounter a bug, the first thing I think of is not how to solve my bug, but whether there is a problem with my process, which of my process will cause the bug, and finally I believe in `go-zero `Can become the preferred framework for **microservice development**. + + +Finally, talk about the pits encountered: + + +- `grpc` + + + +I used `grpc` for the first time, and then I encountered the problem that the field value is not displayed when some characters are empty: + + +It is realized by `jsonpb` in the official library of `grpc`. The official setting has a structure to realize the conversion of `protoc buffer` to JSON structure, and can configure the conversion requirements according to the fields. + + +- Cross-domain issues + + + +It is set in `go-zero`, and it feels no effect. The big guy said that it was set through nginx, but later found that it still didn't work. Recently, I forcibly got a domain name, and I have time to solve it later. + + +- `sqlx` + + + +The `sqlx` problem of `go-zero`, this really took a long time: + + +> `time.Time` is a data structure. Timestamp is used in the database. For example, my field is delete_at. The default database setting is null. When the result is inserted, it reports `Incorrect datetime value: '0000-00-00 'for column'deleted_at' at row 1"}` This error, the query time reported `deleted_at\": unsupported Scan, storing driver.Value type \u003cnil\u003e into type *time.Time"` +> +> I removed this field decisively and added the label `.omitempty` above the field, which seems to be useful too, `db:".omitempty"` + + + +The second is this `Conversion from collation utf8_general_ci into utf8mb4_unicode_ci`. The probable reason for this is that I like to use emj expressions now, and my mysql data cannot be recognized. + + +- data links + + + +`mysql` still follows the original way, modify the encoding format of the configuration file, re-create the database, and set the database encoding to utf8mb4, and the sorting rule is `utf8mb4_unicode_ci`. + + +**In this case, all tables and string fields are in this encoding format. If you don't want all of them, you can set them separately. This is not the point. Because it is easy to set on Navicat, just click manually**。 + + +Here comes the important point: Golang uses the `github.com/go-sql-driver/mysql` driver, which will connect to the `dsn` of `mysql` (because I am using gorm, dsn may be different from the native format. Too the same, but it’s okay, just pay attention to `charset` and `collation`) +`root:password@/name?parseTime=True&loc=Local&charset=utf8` is modified to: +`root:password@/name?parseTime=True&loc=Local&charset=utf8mb4&collation=utf8mb4_unicode_ci` diff --git a/go-zero.dev/en/dev-flow.md b/go-zero.dev/en/dev-flow.md new file mode 100644 index 00000000..5aa5d506 --- /dev/null +++ b/go-zero.dev/en/dev-flow.md @@ -0,0 +1,28 @@ +# Development Flow +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +The development process here is not a concept with our actual business development process. The definition here is limited to the use of go-zero, that is, the development details at the code level. + +## Development Flow +* Goctl environment preparation [1] +* Database Design +* Business development +* New Construction +* Create service catalog +* Create service type (api/rpc/rmq/job/script) +* Write api and proto files +* Code generation +* Generate database access layer code model +* Configuration config, yaml change +* Resource dependency filling (ServiceContext) +* Add middleware +* Business code filling +* Error handling + +> [!TIP] +> [1] [goctl environment](concept-introduction.md) + +## Development Tools +* Visual Studio Code +* Goland (recommended) \ No newline at end of file diff --git a/go-zero.dev/en/dev-specification.md b/go-zero.dev/en/dev-specification.md new file mode 100644 index 00000000..0802620f --- /dev/null +++ b/go-zero.dev/en/dev-specification.md @@ -0,0 +1,27 @@ +# Development Rules +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +In actual business development, in addition to improving business development efficiency, shortening business development cycles, and ensuring high performance and high availability indicators for online business, good programming habits are also one of the basic qualities of a developer. In this chapter, + +We will introduce the coding standards in go-zero. This chapter is an optional chapter. The content is for communication and reference only. This chapter will explain from the following subsections: + +* [Naming Rules](naming-spec.md) +* [Route Rules](route-naming-spec.md) +* [Coding Rules](coding-spec.md) + +## Three principles of development + +### Clarity +The author quoted a quote from `Hal Abelson and Gerald Sussman`: +> Programs must be written for people to read, and only incidentally for machines to execute + +### Simplicity +> Simplicity is prerequisite for reliability + +`Edsger W. Dijkstra` believes that: the prerequisite for reliability is simplicity. We have all encountered in actual development. What is this code written and what it wants to accomplish. Developers don’t understand this code, so they don’t know. How to maintain, this brings complexity, the more complex the program, the harder it is to maintain, and the harder it is to maintain, the program becomes more and more complicated. Therefore, the first thing you should think of when encountering a program becoming complicated is - Refactoring, refactoring will redesign the program and make the program simple. + +### Productivity) +In the go-zero team, this topic has always been emphasized. The productivity of developers is not how many lines of code you have written and how many module developments you have completed, but we need to use various effective ways to take advantage of the limited Time to complete the development to maximize the efficiency, and the birth of Goctl was officially to increase productivity, +Therefore, I very much agree with this development principle. + diff --git a/go-zero.dev/en/doc-contibute.md b/go-zero.dev/en/doc-contibute.md new file mode 100644 index 00000000..bff5f63f --- /dev/null +++ b/go-zero.dev/en/doc-contibute.md @@ -0,0 +1,54 @@ +# Document Contribute +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +## How to contribute documents? +Click the "Edit this page" button at the top to enter the file corresponding to the source code repository, and the developer will submit the modified (added) document in the form of pr, +After we receive the pr, we will conduct a document review, and once the review is passed, the document can be updated. + +![doc-edit](./resource/doc-edit.png) + +## What documents can I contribute? +* Documentation errors +* The documentation is not standardized and incomplete +* Go-zero application practice and experience +* Component Center + +## How soon will the document be updated after the document pr is passed? +After pr accepts, github action will automatically build gitbook and release, so you can view the updated document 1-2 minutes after github action is successful. + +## Documentation contribution notes +* Error correction and improvement of the source document can directly write the original md file +* The newly added component documents need to be typeset and easy to read, and the component documents need to be placed in the [Components](extended-reading.md) subdirectory +* Go-zero application practice sharing can be directly placed in the [Development Practice](practise.md) subdirectory + +## Directory structure specification +* The directory structure should not be too deep, preferably no more than 3 levels +* The component document needs to be attributed to [Component Center] (component-center.md), such as + * [Development Practice](practise.md) + * [logx](logx.md) + * [bloom](bloom.md) + * [executors](executors.md) + * Your document directory name +* Application practice needs to be attributed to [Development Practice](practise.md), such as + * [Development Practice](practise.md) + * [How do I use go-zero to implement a middle-office system] (datacenter.md) + * [Stream data processing tool](stream.md) + * [Summary of online communication issues on October 3](online-exchange.md + * Your document directory name + +## Development Practice Document Template + ```markdown + # Title + + > Author:The author name + > + > Original link: The original link + + some markdown content + ``` + +# Guess you wants +* [Join Us](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) + diff --git a/go-zero.dev/en/error-handle.md b/go-zero.dev/en/error-handle.md new file mode 100644 index 00000000..9f159afc --- /dev/null +++ b/go-zero.dev/en/error-handle.md @@ -0,0 +1,179 @@ +# Error Handling +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +Error handling is an indispensable part of service. In normal business development, we can think that the http status code is not in the `2xx` series, it can be regarded as an http request error. +It is accompanied by error messages in response, but these error messages are all returned in plain text. In addition, I will define some business errors in the business, and the common practice is to pass +The two fields `code` and `msg` are used to describe the business processing results, and it is hoped that the response can be made with the json response body. + +## Business error response format +* Business processing is normal + ```json + { + "code": 0, + "msg": "successful", + "data": { + .... + } + } + ``` + +* Business processing exception + ```json + { + "code": 10001, + "msg": "something wrong" + } + ``` + +## login of user api +Previously, when we handled the login logic when the username did not exist, an error was directly returned. Let's log in and pass a username that does not exist to see the effect. + +```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 + +Username does not exist +``` +Next we will return it in json format + +## Custom error +* First add a `baseerror.go` file in common and fill in the code + ```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, + } + } + + ``` + +* Replace errors in login logic with CodeError custom errors + ```go + if len(strings.TrimSpace(req.Username)) == 0 || len(strings.TrimSpace(req.Password)) == 0 { + return nil, errorx.NewDefaultError("Invalid parameter") + } + + userInfo, err := l.svcCtx.UserModel.FindOneByNumber(req.Username) + switch err { + case nil: + case model.ErrNotFound: + return nil, errorx.NewDefaultError("Username does not exist") + default: + return nil, err + } + + if userInfo.Password != req.Password { + return nil, errorx.NewDefaultError("User password is incorrect") + } + + 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 + ``` + +* Use custom errors + ```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) + + // Custom error + 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() + } + ``` +* Restart service verification + ```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":"Username does not exist"} + ``` diff --git a/go-zero.dev/en/error.md b/go-zero.dev/en/error.md new file mode 100644 index 00000000..68c11a75 --- /dev/null +++ b/go-zero.dev/en/error.md @@ -0,0 +1,48 @@ +# Error +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +## Error reporting on Windows +```text +A required privilege is not held by the client. +```text +Solution: "Run as administrator" goctl will work. + +## grpc error +* Case 1 + ```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. + + ``` + Solution: + ```text + go get -u github.com/golang/protobuf/protoc-gen-go@v1.3.2 + ``` + +## protoc-gen-go installation failed +```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 +``` + +Please make sure `GOPROXY` has been set, see [Go Module Configuration](gomod-config.md) for GOPROXY setting + +## api service failed to start +```text +error: config file etc/user-api.yaml, error: type mismatch for field xx +``` + +Please confirm whether the configuration items in the `user-api.yaml` configuration file have been configured. If there are values, check whether the yaml configuration file conforms to the yaml format. + +## command not found: goctl +``` +command not found: goctl +``` +Please make sure that goctl has been installed or whether goctl has been added to the environment variable \ No newline at end of file diff --git a/go-zero.dev/en/executors.md b/go-zero.dev/en/executors.md new file mode 100644 index 00000000..21142091 --- /dev/null +++ b/go-zero.dev/en/executors.md @@ -0,0 +1,327 @@ +# executors +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +In `go-zero`, `executors` act as a task pool, do multi-task buffering, and use tasks for batch processing. Such as: `clickhouse` large batch `insert`, `sql batch insert`. At the same time, you can also see `executors` in `go-queue` [In `queue`, `ChunkExecutor` is used to limit the byte size of task submission]. + +So when you have the following requirements, you can use this component: + +- Submit tasks in batches +- Buffer part of tasks and submit lazily +- Delay task submission + + + +Before explaining it in detail, let's give a rough overview: +![c42c34e8d33d48ec8a63e56feeae882a](./resource/c42c34e8d33d48ec8a63e56feeae882a.png) +## Interface design + + +Under the `executors` package, there are the following `executors`: + +| Name | Margin value | +| --- | --- | +| `bulkexecutor` | Reach `maxTasks` [Maximum number of tasks] Submit | +| `chunkexecutor` | Reach `maxChunkSize`[Maximum number of bytes] Submit | +| `periodicalexecutor` | `basic executor` | +| `delayexecutor` | Delay the execution of the passed `fn()` | +| `lessexecutor` | | + + + +You will see that except for the special functions of `delay` and `less`, the other three are all combinations of `executor` + `container`: + + +```go +func NewBulkExecutor(execute Execute, opts ...BulkOption) *BulkExecutor { + // Option mode: It appears in many places in go-zero. In multiple configurations, better design ideas + // 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 the function that actually does the execution] [maxTasks execution critical point] + container := &bulkContainer{ + execute: execute, + maxTasks: options.cachedTasks, + } + // 2. It can be seen that the underlying bulkexecutor depends on the periodicalexecutor + executor := &BulkExecutor{ + executor: NewPeriodicalExecutor(options.flushInterval, container), + container: container, + } + + return executor +} +``` + + +And this `container` is an `interface`: + + +```go +TaskContainer interface { + // Add task to container + AddTask(task interface{}) bool + // Is actually to execute the incoming execute func() + Execute(tasks interface{}) + // When the critical value is reached, remove all tasks in the container and pass them to execute func() through the channel for execution + RemoveAll() interface{} +} +``` + + +This shows the dependency between: + + +- `bulkexecutor`:`periodicalexecutor` + `bulkContainer` +- `chunkexecutor`:`periodicalexecutor` + `chunkContainer` + + +> [!TIP] +> So if you want to complete your own `executor`, you can implement these three interfaces of `container`, and then combine with `periodicalexecutor`. + +So back to the picture 👆, our focus is on the `periodicalexecutor`, and see how it is designed? + + +## How to use + + +First look at how to use this component in business: + +There is a timed service to perform data synchronization from `mysql` to `clickhouse` at a fixed time every day: + + +```go +type DailyTask struct { + ckGroup *clickhousex.Cluster + insertExecutor *executors.BulkExecutor + mysqlConn sqlx.SqlConn +} +``` + + +Initialize `bulkExecutor`: + +```go +func (dts *DailyTask) Init() { + // insertIntoCk() is the real insert execution function [requires developers to write specific business logic by themselves] + dts.insertExecutor = executors.NewBulkExecutor( + dts.insertIntoCk, + executors.WithBulkInterval(time.Second*3), // The container will automatically refresh the task to execute every 3s. + executors.WithBulkTasks(10240), // The maximum number of tasks for the container. Generally set to a power of 2 + ) +} +``` + +> [!TIP] +> An additional introduction: `clickhouse` is suitable for mass insertion, because the insert speed is very fast, mass insert can make full use of clickhouse + + +Main business logic preparation: + + +```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] +> You may be wondering why `Flush(), Wait()` is needed, and I will analyze it through the source code later. + +There are 3 steps to use as a whole: + + +- `Add()`: Add to task +- `Flush()`: Refresh tasks in `container` +- `Wait()`: Wait for the completion of all tasks + + + +## Source code analysis + +> [!TIP] +> The main analysis here is `periodicalexecutor`, because the other two commonly used `executors` rely on it. + + + +### Initialization + +```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`: Pass the channel of `tasks` +- `container`: Temporarily store the task of `Add()` +- `confirmChan`: Block `Add()`, at the beginning of this time, `executeTasks()` will let go of blocking +- `ticker`: To prevent the blocking of `Add()`, there will be a chance to execute regularly and release the temporarily stored task in time + + + +### Add() +After initialization, the first step in the business logic is to add task to `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() { + // default false + var start bool + if !pe.guarded { + // backgroundFlush() will reset guarded + pe.guarded = true + start = true + } + pe.lock.Unlock() + // The backgroundFlush() in if will be executed when the first task is added. Background coroutine brush task + if start { + pe.backgroundFlush() + } + }() + // Control maxTask, >=maxTask will pop and return tasks in the container + if pe.container.AddTask(task) { + return pe.container.RemoveAll(), true + } + + return nil, false +} +``` + +In `addAndCheck()`, `AddTask()` is controlling the maximum number of tasks. If it exceeds the number of tasks, `RemoveAll()` will be executed, and the tasks pop of the temporarily stored `container` will be passed to the `commander`, followed by goroutine loop reading , And then execute tasks. + +### backgroundFlush() +Start a background coroutine, and constantly refresh the tasks in the `container`: + +```go +func (pe *PeriodicalExecutor) backgroundFlush() { + // Encapsulate go func(){} + threading.GoSafe(func() { + ticker := pe.newTicker(pe.interval) + defer ticker.Stop() + + var commanded bool + last := timex.Now() + for { + select { + // Get []tasks from channel + case vals := <-pe.commander: + commanded = true + // Substance: wg.Add(1) + pe.enterExecution() + // Let go of the blocking of Add(), and the temporary storage area is also empty at this time. Just start a new task to join + pe.confirmChan <- lang.Placeholder + // Really execute task logic + pe.executeTasks(vals) + last = timex.Now() + case <-ticker.Chan(): + if commanded { + // Due to the randomness of select, if the two conditions are met at the same time and the above is executed at the same time, this treatment is reversed and this paragraph is skipped. + // https://draveness.me/golang/docs/part2-foundation/ch05-keyword/golang-select/ + commanded = false + } else if pe.Flush() { + // The refresh is complete and the timer is cleared. The temporary storage area is empty, start the next timed refresh + last = timex.Now() + } else if timex.Since(last) > pe.interval*idleRound { + // If maxTask is not reached, Flush() err, and last->now is too long, Flush() will be triggered again + // Only when this is reversed will a new backgroundFlush() background coroutine be opened + pe.guarded = false + // Refresh again to prevent missing + pe.Flush() + return + } + } + } + }) +} +``` + +Overall two processes: + +- `commander` receives the tasks passed by `RemoveAll()`, then executes it, and releases the blocking of `Add()` to continue `Add()` +- It’s time for `ticker`, if the first step is not executed, it will automatically `Flush()` and execute the task. + +### Wait() +In `backgroundFlush()`, a function is mentioned: `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() + }) +} +``` +By enumerating in this way, you can know why you have to bring `dts.insertExecutor.Wait()` at the end. Of course, you have to wait for all `goroutine tasks` to complete. + +## Thinking +In looking at the source code, I thought about some other design ideas, do you have similar questions: + +- In the analysis of `executors`, you will find that there are `lock` in many places + +> [!TIP] +> There is a race condition in `go test`, use locking to avoid this situation + +- After analyzing `confirmChan`, it was found that this [submit](https://github.com/zeromicro/go-zero/commit/9d9399ad1014c171cc9bd9c87f78b5d2ac238ce4) only appeared, why is it designed like this? + +> It used to be: `wg.Add(1)` was written in `executeTasks()`; now it is: first `wg.Add(1)`, then release `confirmChan` blocking +> If the execution of `executor func` is blocked, `Add task` is still in progress, because there is no block, it may be executed to `Executor.Wait()` soon, and this is where `wg.Wait()` appears in `wg.Add ()` before execution, this will be `panic` + +For details, please see the latest version of `TestPeriodicalExecutor_WaitFast()`, you may wish to run on this version to reproduce. + +## Summary +There are still a few more analysis of `executors`, I leave it to you to look at the source code. + +In short, the overall design: + +- Follow interface-oriented design +- Flexible use of concurrent tools such as `channel` and `waitgroup` +- The combination of execution unit + storage unit + +There are many useful component tools in `go-zero`. Good use of tools is very helpful to improve service performance and development efficiency. I hope this article can bring you some gains. diff --git a/go-zero.dev/en/extended-reading.md b/go-zero.dev/en/extended-reading.md new file mode 100644 index 00000000..ff491188 --- /dev/null +++ b/go-zero.dev/en/extended-reading.md @@ -0,0 +1,18 @@ +# Components + +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +The component center will include all components in the [go-zero](https://github.com/zeromicro/go-zero) core folder, +Therefore, it will be relatively large, and this resource will continue to be updated, and everyone is welcome to contribute to the document. This section will contain the following directories (in order of document update time): + +* [shorturl](shorturl-en.md) +* [logx](logx.md) +* [bloom](bloom.md) +* [executors](executors.md) +* [fx](fx.md) +* [mysql](mysql.md) +* [redis-lock](redis-lock.md) +* [periodlimit](periodlimit.md) +* [tokenlimit](tokenlimit.md) +* [TimingWheel](timing-wheel.md) diff --git a/go-zero.dev/en/framework-design.md b/go-zero.dev/en/framework-design.md new file mode 100644 index 00000000..1fa8556b --- /dev/null +++ b/go-zero.dev/en/framework-design.md @@ -0,0 +1,13 @@ +# Framework Design +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +![architechture](./resource/architechture.svg) + +This section will explain the design of go-zero framework from the go-zero design philosophy and the best practice catalog of go-zero services. This section will contain the following subsections: + +* [Go-Zero Design](go-zero-design.md) +* [Go-Zero Features](go-zero-features.md) +* [API IDL](api-grammar.md) +* [API Directory Structure](api-dir.md) +* [RPC Directory Structure](rpc-dir.md) diff --git a/go-zero.dev/en/fx.md b/go-zero.dev/en/fx.md new file mode 100644 index 00000000..56a7d3e1 --- /dev/null +++ b/go-zero.dev/en/fx.md @@ -0,0 +1,238 @@ +# fx +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +`fx` is a complete stream processing component. +It is similar to `MapReduce`, `fx` also has a concurrent processing function: `Parallel(fn, options)`. But at the same time it is not only concurrent processing. `From(chan)`, `Map(fn)`, `Filter(fn)`, `Reduce(fn)`, etc., read from the data source into a stream, process the stream data, and finally aggregate the stream data. Is it a bit like Java Lambda? If you were a Java developer before, you can understand the basic design when you see this. + +## Overall API +Let's get an overview of how `fx` is constructed as a whole: +![dc500acd526d40aabfe4f53cf5bd180a_tplv-k3u1fbpfcp-zoom-1.png](./resource/dc500acd526d40aabfe4f53cf5bd180a_tplv-k3u1fbpfcp-zoom-1.png) + +The marked part is the most important part of the entire `fx`: + +1. From APIs such as `From(fn)`, a data stream `Stream` is generated +2. A collection of APIs for converting, aggregating, and evaluating `Stream` + + +So list the currently supported `Stream API`: + +| API | Function | +| --- | --- | +| `Distinct(fn)` | Select a specific item type in fn and de-duplicate it | +| `Filter(fn, option)` | fn specifies specific rules, and the `element` that meets the rules is passed to the next `stream` | +| `Group(fn)` | According to fn, the elements in `stream` are divided into different groups | +| `Head(num)` | Take out the first num elements in `stream` and generate a new `stream` | +| `Map(fn, option)` | Convert each ele to another corresponding ele and pass it to the next `stream` | +| `Merge()` | Combine all `ele` into one `slice` and generate a new `stream` | +| `Reverse()` | Reverse the element in `stream`. [Use double pointer] | +| `Sort(fn)` | Sort elements in `stream` according to fn | +| `Tail(num)` | Take out the last num elements of `stream` to generate a new `stream`. [Using a doubly linked list] | +| `Walk(fn, option)` | Apply fn to every element of `source`. Generate a new `stream` | + + +No longer generates a new `stream`, do the final evaluation operation: + +| API | Function | +| --- | --- | +| `ForAll(fn)` | Process `stream` according to fn, and no longer generate `stream` [evaluation operation] | +| `ForEach(fn)` | Perform fn [evaluation operation] on all elements in `stream` | +| `Parallel(fn, option)` | Concurrently apply the given fn and the given number of workers to each `element`[evaluation operation] | +| `Reduce(fn)` | Directly process `stream` [evaluation operation] | +| `Done()` | Do nothing, wait for all operations to complete | + + + +## How to use? + +```go +result := make(map[string]string) +fx.From(func(source chan<- interface{}) { + for _, item := range data { + source <- item + } +}).Walk(func(item interface{}, pipe chan<- interface{}) { + each := item.(*model.ClassData) + + class, err := l.rpcLogic.GetClassInfo() + if err != nil { + l.Errorf("get class %s failed: %s", each.ClassId, err.Error()) + return + } + + students, err := l.rpcLogic.GetUsersInfo(class.ClassId) + if err != nil { + l.Errorf("get students %s failed: %s", each.ClassId, err.Error()) + return + } + + pipe <- &classObj{ + classId: each.ClassId + studentIds: students + } +}).ForEach(func(item interface{}) { + o := item.(*classObj) + result[o.classId] = o.studentIds +}) +``` + + +1. `From()` generates `stream` from a `slice` +2. `Walk()` receives and a `stream`, transforms and reorganizes each `ele` in the stream to generate a new `stream` +3. Finally, the `stream` output (`fmt.Println`), storage (`map,slice`), and persistence (`db operation`) are performed by the `evaluation operation` + + + +## Briefly analyze + +The function naming in `fx` is semantically. Developers only need to know what kind of conversion is required for the business logic and call the matching function. + + +So here is a brief analysis of a few more typical functions. + +### Walk() + +`Walk()` is implemented as the bottom layer by multiple functions throughout `fx`, such as `Map(), Filter()`, etc. + +So the essence is: `Walk()` is responsible for concurrently applying the passed function to each `ele` of the **input stream** and generating a new `stream`. + +Following the source code, it is divided into two sub-functions: custom count by `worker`, default count is `worker` + +```go +// Custom workers +func (p Stream) walkLimited(fn WalkFunc, option *rxOptions) Stream { + pipe := make(chan interface{}, option.workers) + + go func() { + var wg sync.WaitGroup + // channel<- If the set number of workers is reached, the channel is blocked, so as to control the number of concurrency. + // Simple goroutine pool + pool := make(chan lang.PlaceholderType, option.workers) + + for { + // Each for loop will open a goroutine. If it reaches the number of workers, it blocks + pool <- lang.Placeholder + item, ok := <-p.source + if !ok { + <-pool + break + } + // Use WaitGroup to ensure the integrity of task completion + wg.Add(1) + threading.GoSafe(func() { + defer func() { + wg.Done() + <-pool + }() + + fn(item, pipe) + }) + } + + wg.Wait() + close(pipe) + }() + + return Range(pipe) +} +``` + + +- Use `buffered channel` as a concurrent queue to limit the number of concurrent +- `waitgroup` to ensure the completeness of the task completion + +Another `walkUnlimited()`: also uses `waitgroup` for concurrency control, because there is no custom concurrency limit, so there is no other `channel` for concurrency control. + + +### Tail() + +The introduction of this is mainly because the `ring` is a doubly linked list, and the simple algorithm is still very interesting. + +```go +func (p Stream) Tail(n int64) Stream { + source := make(chan interface{}) + + go func() { + ring := collection.NewRing(int(n)) + // Sequence insertion, the order of the source is consistent with the order of the ring + for item := range p.source { + ring.Add(item) + } + // Take out all the items in the ring + for _, item := range ring.Take() { + source <- item + } + close(source) + }() + + return Range(source) +} +``` + + +As for why `Tail()` can take out the last n of the source, this is left for everyone to fine-tune. Here is my understanding: +![f93c621571074e44a2d403aa25e7db6f_tplv-k3u1fbpfcp-zoom-1.png](./resource/f93c621571074e44a2d403aa25e7db6f_tplv-k3u1fbpfcp-zoom-1.png) + +> [!TIP] +> Suppose there is the following scenario,`Tail(5)` +> - `stream size` :7 +> - `ring size`:5 + + + +Here you can use the method of pulling apart the ring-shaped linked list, **Loop-to-line**,At this point, divide the symmetry axis by the full length, flip the extra elements, and the following elements are the parts needed by `Tail(5)`. + + +> [!TIP] +> The graph is used here for a clearer performance, but everyone should also look at the code. Algorithm to be tested![](https://gw.alipayobjects.com/os/lib/twemoji/11.2.0/2/svg/1f528.svg#align=left&display=inline&height=18&margin=%5Bobject%20Object%5D&originHeight=150&originWidth=150&status=done&style=none&width=18) + + + +### Stream Transform Design + + +Analyzing the entire `fx`, you will find that the overall design follows a design template: + + +```go +func (p Stream) Transform(fn func(item interface{}) interface{}) Stream { + // make channel + source := make(chan interface{}) + // goroutine worker + go func() { + // transform + for item := range p.source { + ... + source <- item + ... + } + ... + // Close the input, but still can output from this stream. Prevent memory leaks + close(source) + }() + // channel -> stream + return Range(source) +} +``` + + +- `channel` as a container for streams +- Open `goroutine` to convert `source`, aggregate, and send to `channel` +- Processed,`close(outputStream)` +- `channel -> stream` + + + +## Summary + +This concludes the basic introduction of `fx`. If you are interested in other API source code, you can follow the API list above to read one by one. + +At the same time, it is also recommended that you take a look at the API of `java stream`, and you can have a deeper understanding of this `stream call`. + +At the same time, there are many useful component tools in `go-zero`. Good use of tools will greatly help improve service performance and development efficiency. I hope this article can bring you some gains. + + +## Reference +- [go-zero](https://github.com/zeromicro/go-zero) +- [Java Stream](https://colobu.com/2016/03/02/Java-Stream/) +- [Stream API in Java 8](https://mp.weixin.qq.com/s/xa98C-QUHRUK0BhWLzI3XQ) diff --git a/go-zero.dev/en/go-queue.md b/go-zero.dev/en/go-queue.md new file mode 100644 index 00000000..e36956b4 --- /dev/null +++ b/go-zero.dev/en/go-queue.md @@ -0,0 +1,305 @@ +# Queue +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +In the development of daily tasks, we will have many asynchronous, batch, timing, and delayed tasks to be processed. There is go-queue in go-zero. It is recommended to use go-queue for processing. Go-queue itself is also developed based on go-zero. There are two modes + + - dq : Depends on beanstalkd, distributed, can be stored, delayed, timing settings, shutdown and restart can be re-executed, messages will not be lost, very simple to use, redis setnx is used in go-queue to ensure that each message is only consumed once, usage scenarios Mainly used for daily tasks. + - kq: Depends on Kafka, so I won’t introduce more about it, the famous Kafka, the usage scenario is mainly to do message queue + + We mainly talk about dq. The use of kq is also the same, but it depends on the bottom layer. If you haven't used beanstalkd, you can google it first. It's still very easy to use. + + + + etc/job.yaml : Configuration file + + ```yaml + Name: job + + Log: + ServiceName: job + Level: info + + # dq depends on Beanstalks, redis, Beanstalks configuration, redis configuration + 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: Parse dq corresponding `etc/*.yaml` configuration + + ```go + /** + * @Description Configuration file + * @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 : Responsible for multi-task registration + + ```go + /** + * @Description Register 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: One of the job business logic + + ```go + /** + * @Description Producer task + * @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") + } + + + ``` + + Another job business logic + + ```go + /** + * @Description Consumer task + * @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 Configuration + * @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 startup file + + ```go + /** + * @Description Startup file + * @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) + + 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 + } + } + } + ``` diff --git a/go-zero.dev/en/go-zero-design.md b/go-zero.dev/en/go-zero-design.md new file mode 100644 index 00000000..b7165cee --- /dev/null +++ b/go-zero.dev/en/go-zero-design.md @@ -0,0 +1,14 @@ +# Go-Zero Design +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +For the design of the microservice framework, we expect that while ensuring the stability of microservices, we must also pay special attention to research and development efficiency. So at the beginning of the design, we have the following guidelines: + +* Keep it simple, first principle +* Flexible design, fault-oriented programming +* Tools are bigger than conventions and documents +* High availability +* High concurrency +* Easy to expand +* Friendly to business development, package complexity +* There is only one way to constrain one thing \ No newline at end of file diff --git a/go-zero.dev/en/go-zero-features.md b/go-zero.dev/en/go-zero-features.md new file mode 100644 index 00000000..324b4bea --- /dev/null +++ b/go-zero.dev/en/go-zero-features.md @@ -0,0 +1,23 @@ +# Go-Zero Features +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +go-zero is a web and rpc framework that integrates various engineering practices. It has the following main features: + +* Powerful tool support, as little code writing as possible +* Minimalist interface +* Fully compatible with net.http +* Support middleware for easy expansion +* High performance +* Fault-oriented programming, flexible design +* Built-in service discovery, load balancing +* Built-in current limiting, fusing, load reduction, and automatic triggering, automatic recovery +* API parameter automatic verification +* Timeout cascade control +* Automatic cache control +* Link tracking, statistical alarm, etc. +* High concurrency support, stably guaranteeing daily traffic peaks during the epidemic + +As shown in the figure below, we have ensured the high availability of the overall service from multiple levels: + +![resilience](https://gitee.com/kevwan/static/raw/master/doc/images/resilience.jpg) \ No newline at end of file diff --git a/go-zero.dev/en/goctl-api.md b/go-zero.dev/en/goctl-api.md new file mode 100644 index 00000000..7e0f7add --- /dev/null +++ b/go-zero.dev/en/goctl-api.md @@ -0,0 +1,71 @@ +# API Commands +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +goctl api is one of the core modules in goctl. It can quickly generate an api service through the .api file with one click. If you just start a go-zero api demo project, +You can complete an api service development and normal operation without even coding. In traditional api projects, we have to create directories at all levels, write structures, +Define routing, add logic files, this series of operations, if calculated according to the business requirements of a protocol, it takes about 5 to 6 minutes for the entire coding to actually enter the writing of business logic. +This does not consider the various errors that may occur during the writing process. With the increase of services and the increase of agreements, the time for this part of the preparation work will increase proportionally. +The goctl api can completely replace you to do this part of the work, no matter how many agreements you have, in the end, it only takes less than 10 seconds to complete. + +> [!TIP] +> The structure is written, and the route definition is replaced by api, so in general, it saves you the time of creating folders, adding various files and resource dependencies. + +## API command description +```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 +``` + +As you can see from the above, according to the different functions, the api contains a lot of self-commands and flags, let’s focus on it here +The `go` subcommand, which function is to generate golang api services, let's take a look at the usage help through `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/zeromicro/go-zero/tree/master/tools/goctl/config/readme.md] +``` + +* --dir: Code output directory +* --api: Specify the api source file +* --style: Specify the file name style of the generated code file, see for details [https://github.com/zeromicro/go-zero/tree/master/tools/goctl/config/readme.md](https://github.com/zeromicro/go-zero/tree/master/tools/goctl/config/readme.md) + +## Usage example +```shell +$ goctl api go -api user.api -dir . -style gozero +``` + +# Guess you wants +* [API IDL](api-grammar.md) +* [API Directory Structure](api-dir.md) \ No newline at end of file diff --git a/go-zero.dev/en/goctl-commands.md b/go-zero.dev/en/goctl-commands.md new file mode 100644 index 00000000..236ae3ca --- /dev/null +++ b/go-zero.dev/en/goctl-commands.md @@ -0,0 +1,273 @@ +# goctl command list +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + + +![goctl](https://zeromicro.github.io/go-zero/en/resource/goctl-command.png) + +# goctl + +## api +(api service related operations) + +### -o +(Generate api file) + +- Example: goctl api -o user.api + +### new +(Quickly create an api service) + +- Example: goctl api new user + +### format +(api format, vscode use) + +- -dir + (Target directory) +- -iu + (Whether to automatically update goctl) +- -stdin + (Whether to read data from standard input) + +### validate +(Verify that the api file is valid) + +- -api + (Specify the api file source) + + - Example: goctl api validate -api user.api + +### doc +(Generate doc markdown) + +- -dir + (Specify the directory) + + - Example: goctl api doc -dir user + +### go +(Generate golang api service) + +- -dir + (Specify the code storage directory) +- -api + (Specify the api file source) +- -force + (Whether to force overwrite existing files) +- -style + (Specify the file name naming style, `gozero`: lowercase, `go_zero`: underscore, `GoZero`: camel case) + +### java +(Generate access api service code-java language) + +- -dir + (Specify the code storage directory) +- -api + (Specify the api file source) + +### ts +(Generate access api service code-ts language) + +- -dir + (Specify the code storage directory) +- -api + (Specify the api file source) +- webapi +- caller +- unwrap + +### dart +(Generate access api service code-dart language) + +- -dir + (Specify code storage target) +- -api + (Specify the api file source) + +### kt +(Generate access api service code-Kotlin language) + +- -dir + (Specify code storage target) +- -api + (Specify the api file source) +- -pkg + (Specify package name) + +### plugin + +- -plugin + Executable file +- -dir + Code storage destination folder +- -api + api source file +- -style + File name formatting + +## template +(Template operation) + +### init +(Cache api/rpc/model template) + +- Example: goctl template init + +### clean +(清空缓存模板) + +- Example: goctl template clean + +### update +(Update template) + +- -category,c + (Specify the group name that needs to be updated api|rpc|model) + + - Example: goctl template update -c api + +### revert +(Restore the specified template file) + +- -category,c + (Specify the group name that needs to be updated api|rpc|model) +- -name,n + (Specify the template file name) + +## config +(Configuration file generation) + +### -path,p +(Specify the configuration file storage directory) + +- Example: goctl config -p user + +## docker +(Generate Dockerfile) + +### -go +(Specify the main function file) + +### -port +(Specify the exposed port) + +## rpc (rpc service related operations) + +### new +(Quickly generate an rpc service) + +- -idea + (Identifies whether the command comes from the idea plug-in and is used for the development and use of the idea plug-in. Please ignore the terminal execution [optional]) +- -style + (Specify the file name naming style, `gozero`: lowercase, `go_zero`: underscore, `GoZero`: camel case) + +### template +(Create a proto template file) + +- -idea + (Identifies whether the command comes from the idea plug-in and is used for the development and use of the idea plug-in. Please ignore the terminal execution [optional]) +- -out,o + (Specify the code storage directory) + +### proto +(Generate rpc service based on proto) + +- -src,s + (Specify the proto file source) +- -proto_path,I + (Specify proto import to find the directory, protoc native commands, for specific usage, please refer to protoc -h to view) +- -dir,d + (Specify the code storage directory) +- -idea + (Identifies whether the command comes from the idea plug-in and is used for the development and use of the idea plug-in. Please ignore the terminal execution [optional]) +- -style + (Specify the file name naming style, `gozero`: lowercase, `go_zero`: underscore, `GoZero`: camel case) + +### model +(Model layer code operation) + +- mysql + (Generate model code from mysql) + + - ddl + (Specify the data source to generate model code for the ddl file) + + - -src,s + (Specify the source of the sql file containing ddl, support wildcard matching) + - -dir,d + (Specify the code storage directory) + - -style + (Specify the file name naming style, `gozero`: lowercase, `go_zero`: underscore, `GoZero`: camel case) + - -cache,c + (Whether the generated code has redis cache logic, bool value) + - -idea + (Identifies whether the command comes from the idea plug-in and is used for the development and use of the idea plug-in. Please ignore the terminal execution [optional]) + + - datasource + (Specify the data source to generate model code from the datasource) + + - -url + (Specify datasource) + - -table,t + (Specify the table name, support wildcards) + - -dir,d + (Specify the code storage directory) + - -style + (Specify the file name naming style, `gozero`: lowercase, `go_zero`: underscore, `GoZero`: camel case) + - -cache,c + (Whether the generated code has redis cache logic, bool value) + - -idea + (Identifies whether the command comes from the idea plug-in and is used for the development and use of the idea plug-in. Please ignore the terminal execution [optional]) +- mongo + (generate model code from mongo) + + -type,t + (specify Go Type name) + -cache,c + (generate code with redis cache logic or not, bool value, default no) + -dir,d + (specify the code generation directory) + -style + (specify filename naming style, gozero:lowercase, go_zero:underscore, GoZero:hump) + +## upgrade +Goctl updated to the latest version + +## kube +Generate k8s deployment file + +### deploy + + +- -name + service name +- -namespace + k8s namespace +- -image + docker image +- -secret + Specify the k8s secret to obtain the mirror +- -requestCpu + Specify the default allocation of cpu +- -requestMem + Specify the default allocation of memory +- -limitCpu + Specify the maximum allocation of cpu +- -limitMem + Specify the maximum amount of memory allocated +- -o + `deployment.yaml` output directory +- -replicas + Specify the replicas +- -revisions + Specify the number of release records to keep +- -port + Specify service port +- -nodePort + Specify the service's external exposure port +- -minReplicas + Specify the minimum number of copies +- -maxReplicas + Specify the maximum number of copies + diff --git a/go-zero.dev/en/goctl-install.md b/go-zero.dev/en/goctl-install.md new file mode 100644 index 00000000..6900e77f --- /dev/null +++ b/go-zero.dev/en/goctl-install.md @@ -0,0 +1,37 @@ +# Goctl Installation +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +## Foreword +Goctl plays a very important role in the development of the go-zero project. It can effectively help developers greatly improve development efficiency, reduce code error rate, and shorten the workload of business development. For more introductions to Goctl, please read [Goctl Introduction ](goctl.md), + +Here we strongly recommend that you install it, because most of the follow-up demonstration examples will use goctl for demonstration. + +## Install(mac&linux) +* download&install + ```shell + GO111MODULE=on GOPROXY=https://goproxy.cn/,direct go get -u github.com/tal-tech/go-zero/tools/goctl + ``` +* Environmental variable detection + + The compiled binary file downloaded by `go get` is located in the `$GOPATH/bin` directory. Make sure that `$GOPATH/bin` has been added to the environment variable. + ```shell + $ sudo vim /etc/paths + ``` + Add the following in the last line + ```text + $GOPATH/bin + ``` + > [!TIP] + > `$GOPATH` is the filepath on your local machine + +* Installation result verification + ```shell + $ goctl -v + ``` + ```text + goctl version 1.1.4 darwin/amd64 + ``` + +> [!TIP] +> For windows users to add environment variables, please Google by yourself. diff --git a/go-zero.dev/en/goctl-model.md b/go-zero.dev/en/goctl-model.md new file mode 100644 index 00000000..5117b5b7 --- /dev/null +++ b/go-zero.dev/en/goctl-model.md @@ -0,0 +1,376 @@ +# Model Commands +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +goctl model is one of the components in the tool module under go-zero. It currently supports the recognition of mysql ddl for model layer code generation. It can be selectively generated with or without redis cache through the command line or idea plug-in (supported soon) The code logic. + +## Quick start + +* Generated by ddl + + ```shell + $ goctl model mysql ddl -src="./*.sql" -dir="./sql/model" -c + ``` + + CURD code can be quickly generated after executing the above command. + + ```text + model + │   ├── error.go + │   └── usermodel.go + ``` + +* Generated by datasource + + ```shell + $ goctl model mysql datasource -url="user:password@tcp(127.0.0.1:3306)/database" -table="*" -dir="./model" + ``` + +* Example code + ```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"` // user + Name string `db:"name"` // user name + Password string `db:"password"` // user password + Mobile string `db:"mobile"` // mobile + Gender string `db:"gender"` // male|female|secret + Nickname string `db:"nickname"` // 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 +``` + +## Generation rules + +* Default rule + + By default, users will create createTime and updateTime fields (ignoring case and underscore naming style) when creating a table, and the default values are both `CURRENT_TIMESTAMP`, and updateTime supports `ON UPDATE CURRENT_TIMESTAMP`. For these two fields, `insert`, It will be removed when `update` is not in the assignment scope. Of course, if you don't need these two fields, it does not matter. +* With cache mode + * 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/zeromicro/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/zeromicro/go-zero/tree/master/tools/goctl/config/readme.md] + --idea for idea plugin [optional] + ``` + + > [!TIP] + > Goctl model mysql ddl/datasource has added a new `--style` parameter to mark the file naming style. + + Currently, only redis cache is supported. If you select the cache mode, the generated `FindOne(ByXxx)`&`Delete` code will generate code with cache logic. Currently, only single index fields (except full-text index) are supported. For joint index By default, we believe that there is no need to bring a cache, and it is not a general-purpose code, so it is not placed in the code generation ranks. For example, the `id`, `name`, and `mobile` fields in the user table in the example are all single-field indexes. + +* Without cache mode + + * 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} + ``` + +Generate code only basic CURD structure. + +## Cache + +For the cache, I chose to list it in the form of question and answer. I think this can more clearly describe the function of the cache in the model. + +* What information will the cache? + + For the primary key field cache, the entire structure information will be cached, while for the single index field (except full-text index), the primary key field value will be cached. + +* Does the data update (`update`) operation clear the cache? + + Yes, but only clear the information in the primary key cache, why? I won't go into details here. + +* Why not generate `updateByXxx` and `deleteByXxx` codes based on single index fields? + + There is no problem in theory, but we believe that the data operations of the model layer are based on the entire structure, including queries. I do not recommend querying only certain fields (no objection), otherwise our cache will be meaningless. + +* Why not support the code generation layer of `findPageLimit` and `findAll`? + + At present, I think that in addition to the basic CURD, the other codes are all business-type codes. I think it is better for developers to write according to business needs. + +# Type conversion rules +| 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 | \ No newline at end of file diff --git a/go-zero.dev/en/goctl-other.md b/go-zero.dev/en/goctl-other.md new file mode 100644 index 00000000..e8249e4f --- /dev/null +++ b/go-zero.dev/en/goctl-other.md @@ -0,0 +1,311 @@ +# More Commands +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +* goctl docker +* goctl kube + +## goctl docker +`goctl docker` can quickly generate a Dockerfile to help developers/operations and maintenance personnel speed up the deployment pace and reduce deployment complexity. + +### Prepare +* docker install + +### Dockerfile note +* Choose the simplest mirror: For example, `alpine`, the entire mirror is about 5M +* Set mirror time zone +```shell +RUN apk add --no-cache tzdata +ENV TZ Asia/Shanghai +``` + +### Multi-stage build +* Otherwise, an executable file will be built in the first stage of construction to ensure that the build process is independent of the host +* The second stage uses the output of the first stage as input to construct the final minimalist image + +### Dockerfile writing process +* First install the goctl tool +```shell +$ GO111MODULE=on GOPROXY=https://goproxy.cn/,direct go get -u github.com/tal-tech/go-zero/tools/goctl +``` + +* Create a hello service under the greet project +```shell +$ goctl api new hello +``` + +The file structure is as follows: +```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 +``` +* Generate a `Dockerfile` in the `hello` directory +```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"] +``` +* To `build` mirror in the `greet` directory +```shell +$ docker build -t hello:v1 -f service/hello/Dockerfile . +``` + +* View mirror +```shell +hello v1 5455f2eaea6b 7 minutes ago 18.1MB +``` + +It can be seen that the mirror size is about 18M. +* Start service +```shell +$ docker run --rm -it -p 8888:8888 hello:v1 +``` +* Test service +```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 summary +The goctl tool greatly simplifies the writing of Dockerfile files, provides best practices out of the box, and supports template customization. + +## goctl kube + +`goctl kube` provides the function of quickly generating a `k8s` deployment file, which can speed up the deployment progress of developers/operations and maintenance personnel and reduce deployment complexity. + +### Have a trouble to write K8S deployment files? + + +- `K8S yaml` has a lot of parameters, need to write and check? +- How to set the number of retained rollback versions? +- How to detect startup success, how to detect live? +- How to allocate and limit resources? +- How to set the time zone? Otherwise, the print log is GMT standard time +- How to expose services for other services to call? +- How to configure horizontal scaling based on CPU and memory usage? + + + +First, you need to know that you have these knowledge points, and secondly, it is not easy to understand all these knowledge points, and again, it is still easy to make mistakes every time you write! + +## Create service image +For demonstration, here we take the `redis:6-alpine` image as an example. + +## 完整 K8S Deployment file writing process + +- First install the `goctl` tool + +```shell +$ GO111MODULE=on GOPROXY=https://goproxy.cn/,direct go get -u github.com/tal-tech/go-zero/tools/goctl +``` + +- One-click generation of K8S deployment files + +```shell +$ goctl kube deploy -name redis -namespace adhoc -image redis:6-alpine -o redis.yaml -port 6379 +``` +The generated `yaml` file is as follows: + + +```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 +``` + + +- Deploy the service, if the `adhoc` namespace does not exist, please create it through `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 +``` + +- View service permission status +``` +$ 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 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 +``` + + +- Test service +``` +$ 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 summary +The `goctl` tool greatly simplifies the writing of K8S yaml files, provides best practices out of the box, and supports template customization. + +# Guess you wants +* [Prepare](prepare.md) +* [API Directory Structure](api-dir.md) +* [API IDL](api-grammar.md) +* [API Configuration](api-config.md) +* [API Commands](goctl-api.md) +* [Docker](https://www.docker.com) +* [K8s](https://kubernetes.io/docs/home/) \ No newline at end of file diff --git a/go-zero.dev/en/goctl-plugin.md b/go-zero.dev/en/goctl-plugin.md new file mode 100644 index 00000000..88ee98ef --- /dev/null +++ b/go-zero.dev/en/goctl-plugin.md @@ -0,0 +1,64 @@ +# Plugin Commands +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +Goctl supports custom plugins for api, so how do I customize a plugin? Let's take a look at an example of how to finally use it below. +```go +$ goctl api plugin -p goctl-android="android -package com.tal" -api user.api -dir . +``` + +The above command can be broken down into the following steps: +* goctl parsing api file +* goctl passes the parsed structure ApiSpec and parameters to the goctl-android executable file +* goctl-android customizes the generation logic according to the ApiSpec structure. + +The first part of this command goctl api plugin -p is a fixed parameter, goctl-android="android -package com.tal" is a plugin parameter, where goctl-android is the plugin binary file, and android -package com.tal is a custom parameter of the plugin , -Api user.api -dir. Is a common custom parameter for goctl. +## How to write a custom plug-in? +A very simple custom plug-in demo is included in the go-zero framework. The code is as follows: +```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()` The function of this line of code is to parse the data passed from goctl, which contains the following parts: + +```go +type Plugin struct { + Api *spec.ApiSpec + Style string + Dir string +} +``` +> [!TIP] +> Api: defines the structure data of the api file +> +> Style: optional, it is used to control file naming conventions +> +> Dir: workDir + + +Complete android plugin demo project based on plugin +[https://github.com/zeromicro/goctl-android](https://github.com/zeromicro/goctl-android) + +# Guess you wants +* [API Directory Structure](api-dir.md) +* [API IDL](api-grammar.md) +* [API Configuration](api-config.md) +* [API Commands](goctl-api.md) \ No newline at end of file diff --git a/go-zero.dev/en/goctl-rpc.md b/go-zero.dev/en/goctl-rpc.md new file mode 100644 index 00000000..58317c9d --- /dev/null +++ b/go-zero.dev/en/goctl-rpc.md @@ -0,0 +1,227 @@ +# RPC Commands +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +Goctl Rpc is a rpc service code generation module under `goctl` scaffolding, which supports proto template generation and rpc service code generation. To generate code through this tool, you only need to pay attention to the business logic writing instead of writing some repetitive code. This allows us to focus on the business, thereby speeding up development efficiency and reducing the code error rate. + +## Features + +* Simple and easy to use +* Quickly improve development efficiency +* Low error rate +* Close to protoc + + +## Quick start + +### The way one: Quickly generate greet service + +Generated by the command `goctl rpc new ${servieName}` + +Such as generating `greet` rpc service: + + ```Bash + goctl rpc new greet + ``` + +The code structure after execution is as follows: + + ```go +. +├── etc // yaml configuration file +│ └── greet.yaml +├── go.mod +├── greet // pb.go folder① +│ └── greet.pb.go +├── greet.go // main entry +├── greet.proto // proto source file +├── greetclient // call logic ② +│ └── greet.go +└── internal + ├── config // yaml configuration corresponding entity + │ └── config.go + ├── logic //business code + │ └── pinglogic.go + ├── server // rpc server + │ └── greetserver.go + └── svc // dependent resources + └── servicecontext.go + ``` + +> ① The name of the pb folder (the old version folder is fixed as pb) is taken from the value of option go_package in the proto file. The last level is converted according to a certain format. If there is no such declaration, it is taken from the value of package. The approximate code is as follows: + +```go + if option.Name == "go_package" { + ret.GoPackage = option.Constant.Source + } + ... + if len(ret.GoPackage) == 0 { + ret.GoPackage = ret.Package.Name + } + ret.PbPackage = GoSanitized(filepath.Base(ret.GoPackage)) + ... +``` +> For GoSanitized method, please refer to google.golang.org/protobuf@v1.25.0/internal/strs/strings.go:71 + +> ② The name of the call layer folder is taken from the name of the service in the proto. If the name of the sercice is equal to the name of the pb folder, the client will be added after service to distinguish between pb and call. + +```go +if strings.ToLower(proto.Service.Name) == strings.ToLower(proto.GoPackage) { + callDir = filepath.Join(ctx.WorkDir, strings.ToLower(stringx.From(proto.Service.Name+"_client").ToCamel())) +} +``` + +rpc one-click generation to solve common problems, see [Error](error.md) + +### The way two: Generate rpc service by specifying proto + +* Generate proto template + + ```Bash + goctl rpc template -o=user.proto + ``` + + ```go + syntax = "proto3"; + + package remote; + + option go_package = "remote"; + + message Request { + string username = 1; + string password = 2; + } + + message Response { + string name = 1; + string gender = 2; + } + + service User { + rpc Login(Request)returns(Response); + } + ``` + +* Generate rpc service code + + ```Bash + goctl rpc proto -src user.proto -dir . + ``` + +## Prepare + +* Installed go environment +* Protoc&protoc-gen-go is installed, and environment variables have been set +* For more questions, please see Notes + +## Usage + +### rpc service generation usage + +```Bash +goctl rpc proto -h +``` + +```Bash +NAME: + goctl rpc proto - generate rpc from proto + +USAGE: + goctl rpc proto [command options] [arguments...] + +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/zeromicro/go-zero/tree/master/tools/goctl/config/readme.md] + --idea whether the command execution environment is from idea plugin. [optional] +``` + +### 参数说明 + +* --src: required, the proto data source, currently supports the generation of a single proto file +* --proto_path: optional. The protoc native subcommand is used to specify where to find proto import. You can specify multiple paths, such as `goctl rpc -I={path1} -I={path2} ...`, in You can leave it blank when there is no import. The current proto path does not need to be specified, it is already built-in. For the detailed usage of `-I`, please refer to `protoc -h` +* --dir: optional, the default is the directory where the proto file is located, the target directory of the generated code +* --style value the file naming format, see [https://github.com/zeromicro/go-zero/tree/master/tools/goctl/config/readme.md] +* --idea: optional, whether it is executed in the idea plug-in, terminal execution can be ignored + + +### What developers need to do + +Pay attention to business code writing, hand over repetitive and business-unrelated work to goctl, after generating the rpc service code, developers only need to modify. + +* Preparation of configuration files in the service (etc/xx.json, internal/config/config.go) +* Writing business logic in the service (internal/logic/xxlogic.go) +* Preparation of resource context in the service (internal/svc/servicecontext.go) + + +### Precautions +* proto does not support the generation of multiple files at the same time +* proto does not support the introduction of external dependency packages, and message does not support inline +* At present, the main file, shared file, and handler file will be forcibly overwritten, and those that need to be written manually by the developer will not be overwritten and generated. This category has the code header + + ```shell + // Code generated by goctl. DO NOT EDIT! + // Source: xxx.proto + ``` + + If it contains the `DO NOT EDIT` logo, please be careful not to write business code in it. + +## proto import +* The requestType and returnType in rpc must be defined in the main proto file. For the message in proto, other proto files can be imported like protoc. + +proto example: + +### Wrong import +```protobuf +syntax = "proto3"; + +package greet; + +option go_package = "greet"; + +import "base/common.proto" + +message Request { + string ping = 1; +} + +message Response { + string pong = 1; +} + +service Greet { + rpc Ping(base.In) returns(base.Out);// request and return do not support import +} + +``` + + +### Import correctly +```protobuf +syntax = "proto3"; + +package greet; + +option go_package = "greet"; + +import "base/common.proto" + +message Request { + base.In in = 1; +} + +message Response { + base.Out out = 2; +} + +service Greet { + rpc Ping(Request) returns(Response); +} +``` + +# Guess you wants +* [RPC Directory Structure](rpc-dir.md) +* [RPC Configuration](rpc-config.md) +* [RPC Implement & Call](rpc-call.md) \ No newline at end of file diff --git a/go-zero.dev/en/goctl.md b/go-zero.dev/en/goctl.md new file mode 100644 index 00000000..e2abd29e --- /dev/null +++ b/go-zero.dev/en/goctl.md @@ -0,0 +1,71 @@ +# Goctl +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +goctl is a code generation tool under the go-zero microservice framework. Using goctl can significantly improve development efficiency and allow developers to focus their time on business development. Its functions include: + +- api service generation +- rpc service generation +- model code generation +- template management + +This section will contain the following: + +* [Commands & Flags](goctl-commands.md) +* [API Commands](goctl-api.md) +* [RPC Commands](goctl-rpc.md) +* [Model Commands](goctl-model.md) +* [Plugin Commands](goctl-plugin.md) +* [More Commands](goctl-other.md) + +## goctl? +Many people will pronounce `goctl` as `go-C-T-L`. This is a wrong way of thinking. You should refer to `go control` and pronounce `ɡō kənˈtrōl`. + +## View version information +```shell +$ goctl -v +``` + +If goctl is installed, it will output text information in the following format: + +```text +goctl version ${version} ${os}/${arch} +``` + +For example output: +```text +goctl version 1.1.5 darwin/amd64 +``` + +Version number description +* version: goctl version number +* os: Current operating system name +* arch: Current system architecture name + +## Install goctl + +### The way one(go get) + +```shell +$ GO111MODULE=on GOPROXY=https://goproxy.cn/,direct go get -u github.com/tal-tech/go-zero/tools/goctl +``` + +Use this command to install the goctl tool into the `GOPATHbin` directory + +### The way two (fork and build) + +Pull a source code from the go-zero code repository `git@github.com:tal-techgo-zero.git`, enter the `toolsgoctl` directory to compile the goctl file, and then add it to the environment variable. + +After the installation is complete, execute `goctl -v`. If the version information is output, the installation is successful, for example: + +```shell +$ goctl -v + +goctl version 1.1.4 darwin/amd64 +``` + +## FAQ +``` +command not found: goctl +``` +Please make sure that goctl has been installed, or whether goctl has been correctly added to the environment variables of the current shell. diff --git a/go-zero.dev/en/golang-install.md b/go-zero.dev/en/golang-install.md new file mode 100644 index 00000000..86ccb9a6 --- /dev/null +++ b/go-zero.dev/en/golang-install.md @@ -0,0 +1,55 @@ +# Golang Installation +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +## Forward +To develop a golang program, the installation of its environment must be indispensable. Here we choose to take 1.15.1 as an example. + +## Official document +[https://golang.google.cn/doc/install](https://golang.google.cn/doc/install) + +## Install Go on macOS + +* Download and install [Go for Mac](https://dl.google.com/go/go1.15.1.darwin-amd64.pkg) +* Verify the installation result + ```shell + $ go version + ``` + ```text + go version go1.15.1 darwin/amd64 + ``` +## Install Go on linux +* Download [Go for Linux](https://golang.org/dl/go1.15.8.linux-amd64.tar.gz) +* Unzip the compressed package to `/usr/local` + ```shell + $ tar -C /usr/local -xzf go1.15.8.linux-amd64.tar.gz + ``` +* Add `/usr/local/go/bin` to environment variables + ```shell + $ $HOME/.profile + ``` + ```shell + export PATH=$PATH:/usr/local/go/bin + ``` + ```shell + $ source $HOME/.profile + ``` +* Verify the installation result + ```shell + $ go version + ``` + ```text + go version go1.15.1 linux/amd64 + ``` +## Install Go on windows +* Download and install [Go for Windows](https://golang.org/dl/go1.15.8.windows-amd64.msi) +* Verify the installation result + ```shell + $ go version + ``` + ```text + go version go1.15.1 windows/amd64 + ``` + +## More +For more operating system installation, see [https://golang.org/dl/](https://golang.org/dl/) diff --git a/go-zero.dev/en/gomod-config.md b/go-zero.dev/en/gomod-config.md new file mode 100644 index 00000000..1b4c825e --- /dev/null +++ b/go-zero.dev/en/gomod-config.md @@ -0,0 +1,39 @@ +# Go Module Configuration +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +## Introduction to Go Module +> Modules are how Go manages dependencies.[1] + +That is, Go Module is a way for Golang to manage dependencies, similar to Maven in Java and Gradle in Android. + +## Module configuration +* Check the status of `GO111MODULE` + ```shell + $ go env GO111MODULE + ``` + ```text + on + ``` +* Turn on `GO111MODULE`, if it is already turned on (that is, execute `go env GO111MODULE` and the result is `on`), please skip it. + ```shell + $ go env -w GO111MODULE="on" + ``` +* Set up `GOPROXY` + ```shell + $ go env -w GOPROXY=https://goproxy.cn + ``` +* Set up `GOMODCACHE` + + view `GOMODCACHE` + ```shell + $ go env GOMODCACHE + ``` + If the directory is not empty or `/dev/null`, please skip it. + ```shell + go env -w GOMODCACHE=$GOPATH/pkg/mod + ``` + + +# Reference +[1] [Go Modules Reference](https://golang.google.cn/ref/mod) \ No newline at end of file diff --git a/go-zero.dev/en/goreading.md b/go-zero.dev/en/goreading.md new file mode 100644 index 00000000..77753948 --- /dev/null +++ b/go-zero.dev/en/goreading.md @@ -0,0 +1,9 @@ +# Night +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +* [2020-08-16 XiaoHeiBan go-zero microservice framework architecture design](https://talkgo.org/t/topic/729) +* [2020-10-03 go-zero microservice framework and online communication](https://talkgo.org/t/topic/1070) +* [In-process shared calls to prevent cache breakdown](https://talkgo.org/t/topic/968) +* [Implement JWT authentication based on go-zero](https://talkgo.org/t/topic/1114) +* [Goodbye go-micro! Enterprise project migration go-zero strategy (1)](https://talkgo.org/t/topic/1607) \ No newline at end of file diff --git a/go-zero.dev/en/gotalk.md b/go-zero.dev/en/gotalk.md new file mode 100644 index 00000000..c9d63c7f --- /dev/null +++ b/go-zero.dev/en/gotalk.md @@ -0,0 +1,5 @@ +# OpenTalk +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +* [OpenTalk 4th - Go-Zero](https://www.bilibili.com/video/BV1Jy4y127Xu) \ No newline at end of file diff --git a/go-zero.dev/en/intellij.md b/go-zero.dev/en/intellij.md new file mode 100644 index 00000000..e836bb72 --- /dev/null +++ b/go-zero.dev/en/intellij.md @@ -0,0 +1,116 @@ +# Intellij Plugin +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +## Go-Zero Plugin + +[go-zero](https://github.com/zeromicro/go-zero) +[license](https://github.com/zeromicro/goctl-intellij/blob/main/LICENSE) +[release](https://github.com/zeromicro/goctl-intellij/releases) +[Java CI with Gradle](https://github.com/zeromicro/goctl-intellij/actions) + +## Introduction +A plug-in tool that supports go-zero api language structure syntax highlighting, detection, and quick generation of api, rpc, and model. + + +## Idea version requirements +* IntelliJ 2019.3+ (Ultimate or Community) +* Goland 2019.3+ +* WebStorm 2019.3+ +* PhpStorm 2019.3+ +* PyCharm 2019.3+ +* RubyMine 2019.3+ +* CLion 2019.3+ + +## Features +* api syntax highlighting +* api syntax and semantic detection +* struct, route, handler repeated definition detection +* type jump to the type declaration position +* Support api, rpc, mode related menu options in the context menu +* Code formatting (option+command+L) +* Code hint + +## Install + +### The way one +Find the latest zip package in the GitHub release, download it and install it locally. (No need to unzip) + +### Thw way two +In the plugin store, search for `Goctl` to install + + +## Preview +![preview](./resource/api-compare.png) + +## Create a new Api(Proto) file +In the project area target folder `right click ->New-> New Api(Proto) File ->Empty File/Api(Proto) Template`, as shown in the figure: +![preview](./resource/api-new.png) + +# Quickly generate api/rpc service +In the target folder `right click->New->Go Zero -> Api Greet Service/Rpc Greet Service` + +![preview](./resource/service.png) + +# Api/Rpc/Model Code generation + +## The way one(Project Panel) + +Corresponding files (api, proto, sql) `right click->New->Go Zero-> Api/Rpc/Model Code`, as shown in the figure: + +![preview](./resource/project_generate_code.png) + +## Thw way two(Editor Panel) +Corresponding files (api, proto, sql) `right click -> Generate-> Api/Rpc/Model Code` + + +# Error message +![context menu](./resource/alert.png) + + +# Live Template +Live Template can speed up our writing of api files. For example, when we enter the `main` keyword in the go file, press the tip and press Enter and insert a piece of template code. +```go +func main(){ + +} +``` +In other words, you will be more familiar with the picture below. Once upon a time, you still defined the template here. +![context menu](./resource/go_live_template.png) + +Let’s enter the instructions for using the template in today’s api grammar. Let’s take a look at the effect of the service template. +![context menu](./resource/live_template.gif) + +First of all, in the previous picture, take a look at several template effective areas in the api file (psiTree element area) +![context menu](./resource/psiTree.png) + +#### Default template and effective scope +| keyword | psiTree effective scope|description| +| ---- | ---- | ---- | +| @doc | ApiService |doc comment template| +| doc | ApiService |doc comment template| +| struct | Struct |struct declaration template| +| info | ApiFile |info block template| +| type | ApiFile |type group template| +| handler | ApiService |handler name template| +| get | ApiService |get method routing template| +| head | ApiService |head method routing template| +| post | ApiService |post method routing template| +| put | ApiService |put method routing template| +| delete | ApiService |delete method routing template| +| connect | ApiService |connect method routing template| +| options | ApiService |options method routing template| +| trace | ApiService |trace method routing template| +| service | ApiFile |service service block template| +| json | Tag、Tag literal |tag template| +| xml | Tag、Tag literal |tag template| +| path | Tag、Tag literal |tag template| +| form | Tag、Tag literal |tag template| + +For the corresponding content of each template, you can view the detailed template content in `Goland(mac Os)->Preference->Editor->Live Templates-> Api|Api Tags`, for example, the json tag template content is +```go +json:"$FIELD_NAME$" +``` +![context menu](./resource/json_tag.png) + + diff --git a/go-zero.dev/en/join-us.md b/go-zero.dev/en/join-us.md new file mode 100644 index 00000000..1114556a --- /dev/null +++ b/go-zero.dev/en/join-us.md @@ -0,0 +1,61 @@ +# Join Us +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + + +## Summary +go-zero + +[go-zero](https://github.com/zeromicro/go-zero) is based on the [MIT License](https://github.com/zeromicro/go-zero/blob/master/LICENSE) open source projects, if you find bugs, new features, etc. in use, you can participate in the contribution of go-zero. We welcome your active participation and will respond to various questions raised by you as soon as possible. , Pr, etc. + +## Contribution form +* [Pull Request](https://github.com/zeromicro/go-zero/pulls) +* [Issue](https://github.com/zeromicro/go-zero/issues) + +## Contribution notes +The code in go-zero's Pull request needs to meet certain specifications +* For naming conventions, please read [naming conventions](naming-spec.md) +* Mainly English annotations +* Remark the functional characteristics when pr, the description needs to be clear and concise +* Increase unit test coverage to 80%+ + +## Pull Request(pr) +* Enter [go-zero](https://github.com/zeromicro/go-zero) project, fork a copy of [go-zero](https://github.com/zeromicro/go-zero) Project to its own GitHub repository. +* Go back to your GitHub homepage and find the `xx/go-zero` project, where xx is your username, such as `anqiansong/go-zero` + + ![fork](./resource/fork.png) +* Clone the code to local + + ![clone](./resource/clone.png) +* Develop code and push to your own GitHub repository +* Enter the go-zero project in your own GitHub, click on the `[Pull requests]` on the floating layer to enter the Compare page. + + ![pr](./resource/new_pr.png) + +* `base repository` choose `tal-tech/go-zero` `base:master`,`head repository` choose `xx/go-zero` `compare:$branch` ,`$branch` is the branch you developed, as shown in the figure: + + ![pr](./resource/compare.png) + +* Click `[Create pull request]` to realize the pr application +* To confirm whether the pr submission is successful, enter [Pull requests](https://github.com/zeromicro/go-zero) of [go-zero](https://github.com/zeromicro/go-zero) /pulls) view, there should be a record of your own submission, the name is your branch name during development. + + ![pr record](./resource/pr_record.png) + +## Issue +In our community, many partners will actively feedback some problems encountered during the use of go-zero. +Due to the large number of people in the community, although we will follow the community dynamics in real time, +the feedback of all questions is random. When our team is still solving a problem raised by a partner, other issues are also fed back, +which may cause the team to easily ignore it. In order to solve everyone's problems one by one, we strongly recommend that everyone use the issue method. +Feedback issues, including but not limited to bug, expected new features, etc., we will also reflect in the issue when we implement a certain new feature. +You can also get the latest trend of go-zero here, and welcome everyone Come and actively participate in the discussion. + +### How to issue +* Click [here](https://github.com/zeromicro/go-zero/issues) to enter go-zero's Issue page or directly visit [https://github.com/zeromicro/go-zero/ issues](https://github.com/zeromicro/go-zero/issues) address +* Click `[New issue]` in the upper right corner to create a new issue +* Fill in the issue title and content +* Click `【Submit new issue】` to submit an issue + + +## Reference + +* [Github Pull request](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/proposing-changes-to-your-work-with-pull-requests) \ No newline at end of file diff --git a/go-zero.dev/en/jwt.md b/go-zero.dev/en/jwt.md new file mode 100644 index 00000000..1b14e5a5 --- /dev/null +++ b/go-zero.dev/en/jwt.md @@ -0,0 +1,240 @@ +# JWT authentication +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +## Summary +> JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and independent method for securely transmitting information as JSON objects between parties. Since this information is digitally signed, it can be verified and trusted. The JWT can be signed using a secret (using the HMAC algorithm) or using a public/private key pair of RSA or ECDSA. + +## When should you use JSON Web Tokens? +* Authorization: This is the most common scenario for using JWT. Once the user is logged in, each subsequent request will include the JWT, allowing the user to access routes, services, and resources that are permitted with that token. Single Sign On is a feature that widely uses JWT nowadays, because of its small overhead and its ability to be easily used across different domains. + +* Information exchange: JSON Web Tokens are a good way of securely transmitting information between parties. Because JWTs can be signed—for example, using public/private key pairs—you can be sure the senders are who they say they are. Additionally, as the signature is calculated using the header and the payload, you can also verify that the content hasn't been tampered with. + +## Why should we use JSON Web Tokens? +As JSON is less verbose than XML, when it is encoded its size is also smaller, making JWT more compact than SAML. This makes JWT a good choice to be passed in HTML and HTTP environments. + +Security-wise, SWT can only be symmetrically signed by a shared secret using the HMAC algorithm. However, JWT and SAML tokens can use a public/private key pair in the form of a X.509 certificate for signing. Signing XML with XML Digital Signature without introducing obscure security holes is very difficult when compared to the simplicity of signing JSON. + +JSON parsers are common in most programming languages because they map directly to objects. Conversely, XML doesn't have a natural document-to-object mapping. This makes it easier to work with JWT than SAML assertions. + +Regarding usage, JWT is used at Internet scale. This highlights the ease of client-side processing of the JSON Web token on multiple platforms, especially mobile. + +> [!TIP] +> All the above content quote from [jwt.io](https://jwt.io/introduction) + +## How to use jwt in go-zero +Jwt authentication is generally used at the api layer. In this demonstration project, we generate jwt token when user api logs in, and verify the user jwt token when searching api for books. + +### user api generates jwt token +Following the content of the [Business Coding](business-coding.md) chapter, we perfect the `getJwtToken` method left over from the previous section, that is, generate the jwt token logic + +#### Add configuration definition and yaml configuration items +```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: The easiest way to generate the key of the jwt token is to use an uuid value. +> +> $AccessExpire: Jwt token validity period, unit: second +> +> For more configuration information, please refer to [API Configuration](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 uses jwt token authentication +#### Write search.api file +```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`: Enable jwt authentication +> +> If the routing requires JWT authentication, you need to declare this syntax flag above the service, such as `/search/do` above +> +> Routes that do not require jwt authentication do not need to be declared, such as `/search/ping` above +> +> For more grammar, please read [API IDL](api-grammar.md) + + +#### Generate code +As described above, there are three ways to generate code, so I won’t go into details here. + + +#### Add yaml configuration items +```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: This value must be consistent with the one declared in the user api. +> +> $AccessExpire: Valid period +> +> Modify the port here to avoid conflicts with user api port 8888 + +### Verify jwt token +* Start user api service, and login + ```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":"xiaoming","gender":"male","accessToken":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MTI4NjcwNzQsImlhdCI6MTYxMjc4MDY3NCwidXNlcklkIjoxfQ.JKa83g9BlEW84IiCXFGwP2aSd0xF3tMnxrOzVebbt80","accessExpire":1612867074,"refreshAfter":1612823874} + ``` +* Start the search api service, call `/search/do` to verify whether the jwt authentication is passed + ```shell + $ go run search.go -f etc/search-api.yaml + ``` + ```text + Starting server at 0.0.0.0:8889... + ``` + Let’s not pass the jwt token and see the result: + ```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 + ``` + Obviously, the jwt authentication failed, and the statusCode of 401 is returned. Next, let's take a jwt token (that is, the `accessToken` returned by the user login) + ```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] + > Service startup error, please check [Error](error.md) + + +At this point, the demonstration of jwt from generation to use is complete. The authentication of jwt token is already encapsulated in go-zero. You only need to simply declare it when defining the service in the api file. + +### Get the information carried in the jwt token +After go-zero is parsed from the jwt token, the kv passed in when the user generates the token will be placed in the Context of http.Request intact, so we can get the value you want through the Context. + +```shell +$ vim /service/search/cmd/api/internal/logic/searchlogic.go +``` +Add a log to output the userId parsed from jwt. +```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 +} +``` +Output +```text +{"@timestamp":"2021-02-09T10:29:09.399+08","level":"info","content":"userId: 1"} +``` + +# Guess you wants +* [JWT](https://jwt.io/) +* [API Configuration](api-config.md) +* [API IDL](api-grammar.md) diff --git a/go-zero.dev/en/learning-resource.md b/go-zero.dev/en/learning-resource.md new file mode 100644 index 00000000..c96e62f2 --- /dev/null +++ b/go-zero.dev/en/learning-resource.md @@ -0,0 +1,8 @@ +# Learning Resources +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +The latest learning resource channel of go-zero will be updated from time to time. Currently, the channels included are: +* [Wechat](wechat.md) +* [Night](goreading.md) +* [OpenTalk](gotalk.md) \ No newline at end of file diff --git a/go-zero.dev/en/log-collection.md b/go-zero.dev/en/log-collection.md new file mode 100644 index 00000000..ee819092 --- /dev/null +++ b/go-zero.dev/en/log-collection.md @@ -0,0 +1,144 @@ +# Log Collection +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +In order to ensure the stable operation of the business and predict the unhealthy risks of the service, the collection of logs can help us observe the current health of the service. +In traditional business development, when there are not many machine deployments, we usually log in directly to the server to view and debug logs. However, as the business increases, services continue to be split. + +The maintenance cost of the service will also become more and more complicated. In a distributed system, there are more server machines, and the service is distributed on different servers. When problems are encountered, +We can't use traditional methods to log in to the server for log investigation and debugging. The complexity can be imagined. + +![log-flow](./resource/log-flow.png) + +> [!TIP] +> If it is a simple single service system, or the service is too small, it is not recommended to use it directly, otherwise it will be counterproductive. + +## Prepare +* kafka +* elasticsearch +* kibana +* filebeat、Log-Pilot(k8s) +* go-stash + +## Filebeat +```shell +$ vim xx/filebeat.yaml +``` + +```yaml +filebeat.inputs: +- type: log + enabled: true + # Turn on json parsing + json.keys_under_root: true + json.add_error_key: true + # Log file path + paths: + - /var/log/order/*.log + +setup.template.settings: + index.number_of_shards: 1 + +# Define kafka topic field +fields: + log_topic: log-collection + +# Export to 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 is the path where filebeat.yaml is located + +## go-stash configuration +* Create a new `config.yaml` file +* Add configuration content + +```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 +``` + +## Start services (start in order) +* Start kafka +* Start elasticsearch +* Start kibana +* Start go-stash +* Start filebeat +* Start the order-api service and its dependent services (order-api service in the go-zero-demo project) + +## Visit kibana +Enter 127.0.0.1:5601 +![log](./resource/log.png) + +> [!TIP] +> Here we only demonstrate the logs generated by logx in the collection service, and log collection in nginx is the same. + + +# Reference +* [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) diff --git a/go-zero.dev/en/logx.md b/go-zero.dev/en/logx.md new file mode 100644 index 00000000..6b8d7ad4 --- /dev/null +++ b/go-zero.dev/en/logx.md @@ -0,0 +1,186 @@ +# logx +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +## Example + +```go +var c logx.LogConf +// Initialize the configuration from the yaml file +conf.MustLoad("config.yaml", &c) + +// logx is initialized according to the configuration +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() +``` + +## Initialization +logx has many configurable items, you can refer to the definition in logx.LogConf. Currently available + +```go +logx.MustSetUp(c) +``` +Perform the initial configuration. If the initial configuration is not performed, all the configurations will use the default configuration. + +## Level +The print log levels supported by logx are: +- info +- error +- server +- fatal +- slow +- stat + +You can use the corresponding method to print out the log of the corresponding level. +At the same time, in order to facilitate debugging and online use, the log printing level can be dynamically adjusted. The level can be set through **logx.SetLevel(uint32)** or through configuration initialization. The currently supported parameters are: + +```go +const ( + // Print all levels of logs + InfoLevel = iotas + // Print errors, slows, stacks logs + ErrorLevel + // Only print server level logs + SevereLevel +) +``` + +## Log mode +At present, the log printing mode is mainly divided into two types, one is file output, and the other is console output. The recommended way, when using k8s, docker and other deployment methods, you can output the log to the console, use the log collector to collect and import it to es for log analysis. If it is a direct deployment method, the file output method can be used, and logx will automatically create log files corresponding to 5 corresponding levels in the specified file directory to save the logs. + +```bash +. +├── access.log +├── error.log +├── severe.log +├── slow.log +└── stat.log +``` + +At the same time, the file will be divided according to the natural day. When the specified number of days is exceeded, the log file will be automatically deleted, packaged and other operations. + +## Disable log +If you don't need log printing, you can use **logx.Close()** to close the log output. Note that when log output is disabled, it cannot be opened again. For details, please refer to the implementation of **logx.RotateLogger** and **logx.DailyRotateRule**. + +## Close log +Because logx uses asynchronous log output, if the log is not closed normally, some logs may be lost. The log output must be turned off where the program exits: +```go +logx.Close() +``` +Log configuration and shutdown related operations have already been done in most places such as rest and zrpc in the framework, so users don't need to care. +At the same time, note that when the log output is turned off, the log cannot be printed again. + +Recommended writing: +```go +import "github.com/tal-tech/go-zero/core/proc" + +// grace close log +proc.AddShutdownListener(func() { + logx.Close() +}) +``` + +## Duration +When we print the log, we may need to print the time-consuming situation, we can use **logx.WithDuration(time.Duration)**, refer to the following example: + +```go +startTime := timex.Now() +// Database query +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) +} +``` + + +Will output the following format: + +```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 - ..."} +``` + +In this way, it is easy to collect statistics about slow sql related information. + +## TraceLog +tracingEntry is customized for link tracing log output. You can print the traceId and spanId information in the context. With our **rest** and **zrpc**, it is easy to complete the related printing of the link log. The example is as follows: + +```go +logx.WithContext(context.Context).Info("This is info!") +``` + + +## SysLog + +Some applications may use system log for log printing. Logx uses the same encapsulation method, which makes it easy to collect log-related logs into logx. + +```go +logx.CollectSysLog() +``` + + + + +# Log configuration related +**LogConf** Define the basic configuration required for the logging system + +The complete definition is as follows: + +```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** defines the log printing method. The default mode is **console**, which will print to the console. + +The currently supported modes are as follows: + +- console + - Print to the console +- file + - Print to access.log, error.log, stat.log and other files in the specified path +- volume + - In order to print to the storage that the mount comes in in k8s, because multiple pods may overwrite the same file, the volume mode automatically recognizes the pod and writes separate log files according to the pod. + +## Path +**Path** defines the output path of the file log, the default value is **logs**. + +## Level +**Level** defines the log printing level, and the default value is **info**. +The currently supported levels are as follows: + +- info +- error +- severe + + + +## Compress +**Compress** defines whether the log needs to be compressed, the default value is **false**. When Mode is file mode, the file will finally be packaged and compressed into a .gz file. + + +## KeepDays +**KeepDays** defines the maximum number of days to keep logs. The default value is 0, which means that old logs will not be deleted. When Mode is file mode, if the maximum retention days are exceeded, the old log files will be deleted. + + +## StackCooldownMillis +**StackCooldownMillis** defines the log output interval, the default is 100 milliseconds. diff --git a/go-zero.dev/en/micro-service.md b/go-zero.dev/en/micro-service.md new file mode 100644 index 00000000..e24a8186 --- /dev/null +++ b/go-zero.dev/en/micro-service.md @@ -0,0 +1,291 @@ +# Microservice +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +In the previous article, we have demonstrated how to quickly create a monolithic service. Next, let’s demonstrate how to quickly create a microservice. +In this section, the api part is actually the same as the creation logic of the monolithic service, except that there is no communication between services in the monolithic service. +And the api service in the microservice will have more rpc call configuration. + +## Forward +This section will briefly demonstrate with an `order service` calling `user service`. The demo code only conveys ideas, and some links will not be listed one by one. + +## Scenario summary +Suppose we are developing a mall project, and the developer Xiao Ming is responsible for the development of the user module (user) and the order module (order). Let's split these two modules into two microservices.① + +> [!NOTE] +> ①: The splitting of microservices is also a science, and we will not discuss the details of how to split microservices here. + +## Demonstration function goal +* Order service (order) provides a query interface +* User service (user) provides a method for order service to obtain user information + +## Service design analysis +According to the scenario summary, we can know that the order is directly facing the user, and the data is accessed through the http protocol, and some basic data of the user needs to be obtained inside the order. Since our service adopts the microservice architecture design, +Then two services (user, order) must exchange data. The data exchange between services is the communication between services. At this point, it is also a developer’s need to adopt a reasonable communication protocol. +For consideration, communication can be carried out through http, rpc and other methods. Here we choose rpc to implement communication between services. I believe that I have already made a better scenario description of "What is the role of rpc service?". +Of course, there is much more than this design analysis before a service is developed, and we will not describe it in detail here. From the above, we need: +* user rpc +* order api + +two services to initially implement this small demo. + +## Create mall project +```shell +$ cd ~/go-zero-demo +$ mkdir mall && cd mall +``` + +## Create user rpc service + +* new user rpc + ```shell + $ cd ~/go-zero-demo/mall + $ mkdir -p user/rpc&&cd user/rpc + ``` + +* Add `user.proto` file, add `getUser` method + + ```shell + $ vim ~/go-zero-demo/mall/user/user.proto + ``` + + ```protobuf + syntax = "proto3"; + + package user; + + option go_package = "user"; + + message IdRequest { + string id = 1; + } + + message UserResponse { + string id = 1; + string name = 2; + string gender = 3; + } + + service User { + rpc getUser(IdRequest) returns(UserResponse); + } + ``` +* Generate code + + ```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] +> If the installed version of `protoc-gen-go` is larger than 1.4.0, it is recommended to add `go_package` to the proto file + + +* Fill in business logic + + ```shell + $ vim internal/logic/getuserlogic.go + ``` + ```go + package logic + + import ( + "context" + + "go-zero-demo/mall/user/internal/svc" + "go-zero-demo/mall/user/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 + } + ``` + +## Create order api service +* Create an `order api` service + + ```shell + $ cd ~/go-zero-demo/mall + $ mkdir -p order/api&&cd order/api + ``` + +* Add api file + ```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) + } + ``` +* Generate `order` service + ```shell + $ goctl api go -api order.api -dir . + Done. + ``` +* Add user rpc configuration + + ```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 + } + ``` +* Add yaml configuration + + ```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 + ``` +* Improve service dependence + + ```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)), + } + } + ``` + +* Add order demo logic + + Add business logic to `getorderlogic` + ```shell + $ vim ~/go-zero-demo/mall/order/api/internal/logic/getorderlogic.go + ``` + ```go + 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("User does not exist") + } + + return &types.OrderReply{ + Id: req.Id, + Name: "test order", + }, nil + ``` + +## Start the service and verify +* Start etcd + ```shell + $ etcd + ``` +* Start user rpc + ```shell + $ go run user.go -f etc/user.yaml + ``` + ```text + Starting rpc server at 127.0.0.1:8080... + ``` + +* Start order api + ```shell + $ go run order.go -f etc/order.yaml + ``` + ```text + Starting server at 0.0.0.0:8888... + ``` +* Access 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] +> The api syntax mentioned in the demo, how to use and install rpc generation, goctl, goctl environment, etc. are not outlined in detail in the quick start. We will have detailed documents to describe in the follow-up. You can also click on the following [Guess you think View] View the corresponding document for quick jump. + +# Source code +[mall source code](https://github.com/zeromicro/go-zero-demo/tree/master/mall) + +# Guess you wants +* [Goctl](goctl.md) +* [API Directory Structure](api-dir.md) +* [API IDL](api-grammar.md) +* [API Configuration](api-config.md) +* [Middleware](middleware.md) +* [RPC Directory Structure](rpc-dir.md) +* [RPC Configuration](rpc-config.md) +* [RPC Implement & Call](rpc-call.md) diff --git a/go-zero.dev/en/middleware.md b/go-zero.dev/en/middleware.md new file mode 100644 index 00000000..f3abea01 --- /dev/null +++ b/go-zero.dev/en/middleware.md @@ -0,0 +1,127 @@ +# Middleware +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +In the previous section, we demonstrated how to use jwt authentication. I believe you have mastered the basic use of jwt. In this section, let’s take a look at how to use api service middleware. + +## Middleware classification +In go-zero, middleware can be divided into routing middleware and global middleware. Routing middleware refers to certain specific routes that need to implement middleware logic, which is similar to jwt and does not place the routes under `jwt:xxx` Does not use middleware functions, +The service scope of global middleware is the entire service. + +## Middleware use +Here we take the `search` service as an example to demonstrate the use of middleware + +### Routing middleware +* Rewrite the `search.api` file and add the `middleware` declaration + ```shell + $ cd service/search/cmd/api + $ vim search.api + ``` + ```text + type SearchReq struct {} + + type SearchReply struct {} + + @server( + jwt: Auth + middleware: Example // Routing middleware declaration + ) + service search-api { + @handler search + get /search/do (SearchReq) returns (SearchReply) + } + ``` +* Regenerate the api code + ```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. + ``` + After the generation is completed, there will be an additional `middleware` directory under the `internal` directory, which is the middleware file, and the implementation logic of the subsequent middleware is also written here. +* Improve resource dependency `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, + } + } + ``` +* Write middleware logic + Only one line of log is added here, with the content example middle. If the service runs and outputs example middle, it means that the middleware is in use. + + ```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) + } + } + ``` +* Start service verification + ```text + {"@timestamp":"2021-02-09T11:32:57.931+08","level":"info","content":"example middle"} + ``` + +### Global middleware +call `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() + + // Global middleware + 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"} +``` \ No newline at end of file diff --git a/go-zero.dev/en/model-gen.md b/go-zero.dev/en/model-gen.md new file mode 100644 index 00000000..0837d399 --- /dev/null +++ b/go-zero.dev/en/model-gen.md @@ -0,0 +1,59 @@ +# Model Generation +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + + +First, after downloading the [demo project](https://go-zero.dev/en/resource/book.zip), we will use the user's model to demonstrate the code generation. + +## Forward +Model is a bridge for services to access the persistent data layer. The persistent data of the business often exists in databases such as mysql and mongo. We all know that the operation of a database is nothing more than CURD. +And these tasks will also take up part of the time for development. I once wrote 40 model files when writing a business. According to the complexity of different business requirements, on average, each model file is almost required. +10 minutes, for 40 files, 400 minutes of working time, almost a day's workload, and the goctl tool can complete the 400 minutes of work in 10 seconds. + +## Prepare +Enter the demo project `book`, find the` user.sql` file under `user/model`, and execute the table creation in your own database. + +## Code generation (with cache) +### The way one(ddl) +Enter the `service/user/model` directory and execute the command +```shell +$ cd service/user/model +$ goctl model mysql ddl -src user.sql -dir . -c +``` +```text +Done. +``` + +### The way two(datasource) +```shell +$ goctl model mysql datasource -url="$datasource" -table="user" -c -dir . +``` +```text +Done. +``` +> [!TIP] +> `$datasource` is the database connection address + +### The way three(intellij plugin) +In Goland, right-click `user.sql`, enter and click `New`->`Go Zero`->`Model Code` to generate it, or open the `user.sql` file, +Enter the editing area, use the shortcut key `Command+N` (for macOS) or `alt+insert` (for windows), select `Mode Code`. + +![model generation](https://zeromicro.github.io/go-zero-pages/resource/intellij-model.png) + +> [!TIP] +> The intellij plug-in generation needs to install the goctl plug-in, see [intellij plugin](intellij.md) for details + +## Verify the generated model file +view tree +```shell +$ tree +``` +```text +. +├── user.sql +├── usermodel.go +└── vars.go +``` + +# Guess you wants +[Model Commands](goctl-model.md) diff --git a/go-zero.dev/en/monolithic-service.md b/go-zero.dev/en/monolithic-service.md new file mode 100644 index 00000000..f3641d20 --- /dev/null +++ b/go-zero.dev/en/monolithic-service.md @@ -0,0 +1,94 @@ +# Monolithic Service +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +## Forward +Since go-zero integrates web/rpc, some friends in the community will ask me whether go-zero is positioned as a microservice framework. +The answer is no. Although go-zero integrates many functions, you can use any one of them independently, or you can develop a single service. + +It is not that every service must adopt the design of the microservice architecture. For this point, you can take a look at the fourth issue of the author (kevin) [OpenTalk](https://www.bilibili.com/video/BV1Jy4y127Xu) , Which has a detailed explanation on this. + +## Create greet service +```shell +$ cd ~/go-zero-demo +$ goctl api new greet +Done. +``` + +Take a look at the structure of the `greet` service +```shell +$ cd greet +$ tree +``` +```text +. +├── etc +│   └── greet-api.yaml +├── go.mod +├── greet.api +├── greet.go +└── internal + ├── config + │   └── config.go + ├── handler + │   ├── greethandler.go + │   └── routes.go + ├── logic + │   └── greetlogic.go + ├── svc + │   └── servicecontext.go + └── types + └── types.go +``` +It can be observed from the above directory structure that although the `greet` service is small, it has "all internal organs". Next, we can write business code in `greetlogic.go`. + +## Write logic +```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 +} +``` + +## Start and access the service + +* Start service + ```shell + $ cd ~/go-zer-demo/greet + $ go run greet.go -f etc/greet-api.yaml + ``` + ```text + Starting server at 0.0.0.0:8888... + ``` + +* Access service + ```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"} + ``` + +# Source code +[greet source code](https://github.com/zeromicro/go-zero-demo/tree/master/greet) + +# Guess you wants +* [Goctl](goctl.md) +* [API Directory Structure](api-dir.md) +* [API IDL](api-grammar.md) +* [API Configuration](api-config.md) +* [Middleware](middleware.md) + + + diff --git a/go-zero.dev/en/mysql.md b/go-zero.dev/en/mysql.md new file mode 100644 index 00000000..1a41b363 --- /dev/null +++ b/go-zero.dev/en/mysql.md @@ -0,0 +1,182 @@ +# Mysql +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +`go-zero` provides easier operation of `mysql` API. + +> [!TIP] +> But `stores/mysql` positioning is not an `orm` framework. If you need to generate `model` layer code through `sql/scheme` -> `model/struct` reverse engineering, developers can use [goctl model](https://go-zero.dev/cn/goctl-model.html), this is an excellent feature. + + + +## Features + +- Provides a more developer-friendly API compared to native +- Complete the automatic assignment of `queryField -> struct` +- Insert "bulkinserter" in batches +- Comes with fuse +- API has been continuously tested by several services +- Provide `partial assignment` feature, do not force strict assignment of `struct` + + + +## Connection +Let's use an example to briefly explain how to create a `mysql` connected model: +```go +// 1. Quickly connect to a mysql +// datasource: mysql dsn +heraMysql := sqlx.NewMysql(datasource) + +// 2. Call in the `servicecontext`, understand the logic layer call of the model upper layer +model.NewMysqlModel(heraMysql, tablename), + +// 3. model layer mysql operation +func NewMysqlModel(conn sqlx.SqlConn, table string) *MysqlModel { + defer func() { + recover() + }() + // 4. Create a batch 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 + +Prepare an `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"` + MobilePhone string `db:"mobile_phone"` +} +``` +Among them, `userBuilderQueryRows`: `go-zero` provides `struct -> [field...]` conversion. Developers can use this as a template directly. +### insert +```go +// An actual insert model layer operation +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 +} +``` + +- Splicing `insertsql` +- Pass in `insertsql` and the `struct field` corresponding to the placeholder -> `con.Exex(insertsql, field...)` + + +> [!WARNING] +> `conn.Exec(sql, args...)`: `args...` needs to correspond to the placeholder in `sql`. Otherwise, there will be problems with assignment exceptions. + + +`go-zero` unified and abstracted operations involving `mysql` modification as `Exec()`. So the `insert/update/delete` operations are essentially the same. For the remaining two operations, the developer can try the above `insert` process. + + +### query + + +You only need to pass in the `querysql` and `model` structure, and you can get the assigned `model`. No need for developers to manually assign values. +```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 +} +``` + +- Declare `model struct`, splicing `querysql` +- `conn.QueryRow(&model, querysql, args...)`: `args...` corresponds to the placeholder in `querysql`. + + + +> [!WARNING] +> The first parameter in `QueryRow()` needs to be passed in `Ptr` "The bottom layer needs to be reflected to assign a value to `struct`" + +The above is to query one record, if you need to query multiple records, you can use `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 +} +``` +The difference from `QueryRow()` is: `model` needs to be set to `Slice`, because it is to query multiple rows, and multiple `model`s need to be assigned. But at the same time you need to pay attention to ️: the first parameter needs to be passed in `Ptr` + +### querypartial + + +In terms of use, it is no different from the above-mentioned `QueryRow()`, "this reflects the highly abstract design of `go-zero`." + + +the difference: + +- `QueryRow()`: `len(querysql fields) == len(struct)`, and one-to-one correspondence +- `QueryRowPartial()` :`len(querysql fields) <= len(struct)` + + + +numA: Number of database fields; numB: the number of defined `struct` attributes. +If `numA [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +In any language development, there are some naming conventions in the language field, good +* Reduce code reading costs +* Reduce maintenance difficulty +* Reduce code complexity + +## Specification suggestion +In our actual development, many developers may transfer from one language to another language field. After switching to another language, +We will all retain the programming habits of the old language. Here, what I suggest is that although some previous specifications of different languages may be the same, +But we'd better be familiar with some official demos to gradually adapt to the programming specifications of the current language, rather than directly migrating the programming specifications of the original language. + +## Naming guidelines +* When the distance between the definition and the last use of the variable name is short, the short name looks better. +* Variable naming should try to describe its content, not type +* Constant naming should try to describe its value, not how to use this value +* When encountering for, if and other loops or branches, single letter names are recommended to identify parameters and return values +* It is recommended to use words to name method, interface, type, and package +* The package name is also part of the naming, please use it as much as possible +* Use a consistent naming style + +## File naming guidelines +* All lowercase +* Avoid underscores (_) except for unit test +* The file name should not be too long + +## Variable naming convention reference +* Initial lowercase +* Hump naming +* See the name to know the meaning, avoid pinyin instead of English +* It is not recommended including an underscore (_) +* It is not recommended including numbers + +**Scope of application** +* Local variables +* Function parameter output, input parameter + +## Function and constant naming convention +* Camel case naming +* The first letter of the exported must be capitalized +* The first letter must be lowercase if it cannot be exported +* Avoid the combination of all uppercase and underscore (_) + + +> [!TIP] +> If it is a go-zero code contribution, you must strictly follow this naming convention + + +# Reference +* [Practical Go: Real world advice for writing maintainable Go programs](https://dave.cheney.net/practical-go/presentations/gophercon-singapore-2019.html#_simplicity) \ No newline at end of file diff --git a/go-zero.dev/en/online-exchange.md b/go-zero.dev/en/online-exchange.md new file mode 100644 index 00000000..35271a26 --- /dev/null +++ b/go-zero.dev/en/online-exchange.md @@ -0,0 +1,156 @@ +# Summary of online communication issues on October 3,2020 +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +- Go-zero applicable scenarios + - I hope to talk about the application scenarios and the advantages of each scenario + - Highly concurrent microservice system + - Support tens of millions of daily activities, millions of QPS + - Complete microservice governance capabilities + - Support custom middleware + - Well managed database and cache + - Effectively isolate faults + - Monolithic system with low concurrency + - This kind of system can directly use the api layer without rpc service + - Use scenarios and use cases of each function + - Limiting + - Fuse + - Load reduction + - time out + - Observability +- The actual experience of go-zero + - The service is stable + - Front-end and back-end interface consistency, one api file can generate front-end and back-end code + - Less specification and less code means less bugs + - Eliminate api documents, greatly reducing communication costs + - The code structure is completely consistent, easy to maintain and take over +- Project structure of microservices, CICD processing of monorepo + +``` + 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 +``` + +The CI of the mono repo is made through gitlab, and the CD uses jenkins +CI is as strict as possible, such as -race, using tools such as sonar +CD has development, testing, pre-release, grayscale and formal clusters +If it is in grayscale at 6 p.m. and there is no fault, it will automatically synchronize to the official cluster at 10 the next day +The formal cluster is divided into multiple k8s clusters, which can effectively prevent a single cluster from failing, just remove it directly, and the cluster upgrade is better +- How to deploy and how to monitor? + - The full amount of K8S is automatically packaged into a docker image through jenkins, and the tag is packaged according to the time, so that you can see which day of the image is at a glance + - As mentioned above, pre-release -> grayscale -> formal + - Prometheus+ self-built dashboard service + - Detect service and request exceptions based on logs +- If you plan to change the go-zero framework to refactor your business, how can you make the online business stable and safe for users to switch without feeling? In addition, how to divide the service under consultation? + - Gradually replace, from outside to inside, add a proxy to proofread, you can switch after proofreading a week + - If there is a database reconstruction, you need to do a good job of synchronizing the old and the new + - Service division is based on business, following the principle of coarse to fine, avoiding one api and one microservice + - Data splitting is particularly important for microservices. The upper layer is easy to split, and the data is difficult to split. As far as possible, ensure that the data is split according to the business +- Service discovery + - Service discovery etcd key design + - Service key + timestamp, the probability of timestamp conflict in the number of service processes is extremely low, ignore it + - etcd service discovery and management, exception capture and exception handling + - Why k8s also uses etcd for service discovery, because the refresh of dns is delayed, resulting in a large number of failures in rolling updates, and etcd can achieve completely lossless updates + - The etcd cluster is directly deployed in the k8s cluster, because there are multiple formal clusters, clusters are single-pointed and registered to avoid confusion + - Automatically detect and refresh for etcd abnormalities or leader switching. When etcd has abnormalities that cannot be recovered, the service list will not be refreshed to ensure that the services are still available +- Cache design and use cases + - Distributed multiple redis clusters, dozens of largest online clusters provide caching services for the same service + - Seamless expansion and contraction + - There is no cache without expiration time to avoid a large amount of infrequently used data occupying resources, the default is one week + - Cache penetration, no data will be cached for one minute for a short period of time to avoid the system crashing due to interface brushing or a large number of non-existent data requests + - Cache breakdown, a process will only refresh the same data once, avoiding a large number of hot data being loaded at the same time + - Cache avalanche, automatically jitter the cache expiration time, with a standard deviation of 5%, so that the expiration time of a week is distributed within 16 hours, effectively preventing avalanches + - Our online database has a cache, otherwise it will not be able to support massive concurrency + - Automatic cache management has been built into go-zero, and code can be automatically generated through goctl +- Can you explain the design ideas of middleware and interceptor? + + - Onion model + - This middleware processes, such as current limiting, fusing, etc., and then decides whether to call next + - next call + - Process the return result of the next call +- How to implement the transaction processing of microservices, the design and implementation of gozero distributed transactions, and what good middleware recommendations are there? + - 2PC, two-phase submission + - TCC, Try-Confirm-Cancel + - Message queue, maximum attempt + - Manual compensation +- How to design better multi-level goroutine exception capture? + - Microservice system request exceptions should be isolated, and a single exception request should not crash the entire process + - go-zero comes with RunSafe/GoSafe to prevent a single abnormal request from causing the process to crash + - Monitoring needs to keep up to prevent abnormal excess without knowing it + - The contradiction between fail fast and fault isolation +- Generation and use of k8s configuration (gateway, service, slb) + - K8s yaml file is automatically generated internally, which is too dependent on configuration and not open source + - I plan to add a k8s configuration template to the bookstore example + - slb->nginx->nodeport->api gateway->rpc service +- Gateway current limiting, fusing and load reduction + - There are two types of current limiting: concurrency control and distributed current limiting + - Concurrency control is used to prevent instantaneous excessive requests and protect the system from being overwhelmed + - Distributed current limit is used to configure different quotas for different services + - Fuse is to protect dependent services. When a service has a large number of exceptions, the caller should protect it so that it has a chance to return to normal and also achieve the effect of fail fast + - Downloading is to protect the current process from exhausting its resources and fall into complete unavailability, ensuring that the maximum amount of requests that can be carried is served as well as possible + - Load reduction and k8s can effectively protect k8s expansion, k8s expansion in minutes, go-zero load reduction in seconds +- Introduce useful components in core, such as timingwheel, etc., and talk about design ideas + - Bloom filter + - In-process cache + - RollingWindow + - TimingWheel + - Various executors + - fx package, map/reduce/filter/sort/group/distinct/head/tail... + - Consistent hash implementation + - Distributed current limiting implementation + - mapreduce, with cancel ability + - There are a lot of concurrency tools in the syncx package +- How to quickly add a kind of rpc protocol support, change the cross-machine discovery to the local node adjustment, and turn off the complex filter and load balancing functions + - go-zero has a relatively close relationship with grpc, and it did not consider supporting protocols other than grpc at the beginning of the design + - If you want to increase it, you can only fork out and change it. + - You can use the direct scheme directly by adjusting the machine + - Why remove filter and load balancing? If you want to go, fork is changed, but there is no need +- The design and implementation ideas of log and monitoring and link tracking, it is best to have a rough diagram + - Log and monitoring We use prometheus, customize the dashboard service, bundle and submit data (every minute) + - Link tracking can see the calling relationship and automatically record the trace log +![](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) +- Is there any pooling technique useful for the go-zero framework? If so, in which core code can you refer to + - Generally do not need to optimize in advance, over-optimization is a taboo + - Core/syncx/pool.go defines a general pooling technology with expiration time +- Go-zero uses those performance test method frameworks, is there a code reference? You can talk about ideas and experience + - go benchmark + - Stress testing can be scaled up according to the estimated ratio by using existing business log samples + - The pressure test must be pressured until the system cannot be carried, see where the first bottleneck is, and then pressure again after the change, and cycle +- Talk about the abstract experience and experience of the code + - Don’t repeat yourself + - You may not need it. Before, business developers often asked me if I could add this function or that function. I usually ask the deep-level purpose carefully. In many cases, I find that this function is redundant, and it is the best practice to not need it. + - Martin Fowler proposed the principle of abstracting after three occurrences. Sometimes some colleagues will ask me to add a function to the framework. After I think about it, I often answer this. You write it in the business layer first. If there is a need in other places, you will tell me again, and it will appear three times. I will consider integrating into the framework + - A file should only do one thing as much as possible, each file should be controlled within 200 lines as much as possible, and a function should be controlled within 50 lines as much as possible, so that you can see the entire function without scrolling + - Need the ability to abstract and refine, think more, often look back and think about the previous architecture or implementation +- Will you publish a book on the go-zero framework from design to practice? What is the future development plan of the framework? + - There is no book publishing plan, and a good framework is the most important + - Continue to focus on engineering efficiency + - Improve service governance capabilities + - Help business development land as quickly as possible diff --git a/go-zero.dev/en/periodlimit.md b/go-zero.dev/en/periodlimit.md new file mode 100644 index 00000000..4f0fd426 --- /dev/null +++ b/go-zero.dev/en/periodlimit.md @@ -0,0 +1,128 @@ +# periodlimit +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +Whether in a single service or in a microservice, the API interface provided by the developer for the front end has an upper limit of access. When the frequency of access or the amount of concurrency exceeds its tolerance, we must consider current limit to ensure the interface. Availability or degraded availability. That is, the interface also needs to be installed with a fuse to prevent the system from being paralyzed due to excessive pressure on the system by unexpected requests. + + +This article will introduce `periodlimit`. +## Usage +```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` adopts a **sliding window** counting method to calculate the number of accesses to the same resource within a period of time. If it exceeds the specified `limit`, access is denied. Of course, if you are accessing different resources within a period of time, the amount of access to each resource does not exceed the `limit`. In this case, a large number of requests are allowed to come in. + + +In a distributed system, there are multiple microservices to provide services. So when instantaneous traffic accesses the same resource at the same time, how to make the counter count normally in the distributed system? At the same time, when computing resources are accessed, multiple calculations may be involved. How to ensure the atomicity of calculations? + + +- `go-zero` counts resource visits with the help of `incrby` of `redis` +- Use `lua script` to do the whole window calculation to ensure the atomicity of calculation + + + +Let's take a look at several key attributes controlled by `lua script`: + +| **argument** | **mean** | +| --- | --- | +| key[1] | Logo for access to resources | +| ARGV[1] | limit => the total number of requests, if it exceeds the rate limit. Can be set to QPS | +| ARGV[2] | window size => sliding window, use ttl to simulate the effect of sliding | + +```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) +-- If it is the first visit, set the expiration time => TTL = window size +-- Because it only limits the number of visits for a period +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 +``` +As for the above `return code`, return it to the caller. The caller decides to request subsequent operations: + +| **return code** | **tag** | call code | **mean** | +| --- | --- | --- | --- | +| 0 | OverQuota | 3 | **over limit** | +| 1 | Allowed | 1 | **in limit** | +| 2 | HitQuota | 2 | **hit limit** | + +The following picture describes the process of request entry and the subsequent situation when the request triggers `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) +## Subsequent processing + + +If a large batch of requests comes in at a certain point in the service, the `periodlimit` reaches the `limit` threshold in a short period of time, and the set time range is far from reaching. The processing of subsequent requests becomes a problem. + + +It is not processed in `periodlimit`, but `code` is returned. The processing of subsequent requests is left to the developer. + + +1. If it is not processed, it is simply to reject the request +2. If these requests need to be processed, developers can use `mq` to buffer the requests to ease the pressure of the requests +3. Use `tokenlimit` to allow temporary traffic impact + + + +So in the next article, we will talk about `tokenlimit` + + +## Summary +The `periodlimit` current limiting scheme in `go-zero` is based on `redis` counters. By calling `redis lua script`, it guarantees the atomicity of the counting process and guarantees that the counting is normal under distributed conditions. However, this scheme has disadvantages because it needs to record all behavior records within the time window. If this amount is particularly large, memory consumption will become very serious. + + +## Reference + +- [go-zero periodlimit](https://github.com/zeromicro/go-zero/blob/master/core/limit/periodlimit.go) +- [Distributed service current limit actual combat, has already lined up the pits for you](https://www.infoq.cn/article/Qg2tX8fyw5Vt-f3HH673) +- [tokenlimit](tokenlimit.md) + + + + + diff --git a/go-zero.dev/en/plugin-center.md b/go-zero.dev/en/plugin-center.md new file mode 100644 index 00000000..1e7c37bf --- /dev/null +++ b/go-zero.dev/en/plugin-center.md @@ -0,0 +1,19 @@ +# Plugins +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +The goctl api provides plugin commands to support the function extension of the api. When the functions in the goctl api do not satisfy your use, +Or you need to extend goctl api function customization, then the plug-in function will be very suitable for developers to be self-sufficient, see for details +[goctl plugin](goctl-plugin.md) + +## Plugin resources +* [goctl-go-compact](https://github.com/zeromicro/goctl-go-compact) + Goctl's default route merges one file into one file +* [goctl-swagger](https://github.com/zeromicro/goctl-swagger) + Generate swagger documents through api files +* [goctl-php](https://github.com/zeromicro/goctl-php) + goctl-php is a plug-in based on goctl, used to generate php call end (server end) http server request code + +# Guess you wants +* [Plugin Commands](goctl-plugin.md) +* [API IDL](api-grammar.md) \ No newline at end of file diff --git a/go-zero.dev/en/practise.md b/go-zero.dev/en/practise.md new file mode 100644 index 00000000..5962d43c --- /dev/null +++ b/go-zero.dev/en/practise.md @@ -0,0 +1,10 @@ +# User Practise +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +* [Persistent layer cache](redis-cache.md) +* [Business layer cache](buiness-cache.md) +* [Queue](go-queue.md) +* [Middle Ground System](datacenter.md) +* [Stream Handler](stream.md) +* [Online Exchange](online-exchange.md) \ No newline at end of file diff --git a/go-zero.dev/en/prepare-other.md b/go-zero.dev/en/prepare-other.md new file mode 100644 index 00000000..7af8830d --- /dev/null +++ b/go-zero.dev/en/prepare-other.md @@ -0,0 +1,12 @@ +# Other +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +Before, we have prepared the Go environment, Go Module configuration, Goctl, protoc&protoc-gen-go installation, these are the environments that developers must prepare during the development phase, and you can optionally install the next environment, +Because these environments generally exist on the server (installation work, operation and maintenance will be completed for you), but in order to complete the follow-up **demonstration** process, I suggest you install it locally, because most of our demo environments will be Locally based. +The following only gives the necessary preparatory work, and does not give a detailed introduction in the length of the document. + +## Other environment +* [etcd](https://etcd.io/docs/current/rfc/v3api/) +* [redis](https://redis.io/) +* [mysql](https://www.mysql.com/) \ No newline at end of file diff --git a/go-zero.dev/en/prepare.md b/go-zero.dev/en/prepare.md new file mode 100644 index 00000000..383d9945 --- /dev/null +++ b/go-zero.dev/en/prepare.md @@ -0,0 +1,12 @@ +# Prepare +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +Before officially entering the actual development, we need to do some preparations, such as: installation of the Go environment, installation of tools used for grpc code generation, +Installation of the necessary tool Goctl, Golang environment configuration, etc., this section will contain the following subsections: + +* [Golang Installation](golang-install.md) +* [Go Module Configuration](gomod-config.md) +* [Goctl Installation](goctl-install.md) +* [protoc & protoc-gen-go Installation](protoc-install.md) +* [More](prepare-other.md) \ No newline at end of file diff --git a/go-zero.dev/en/project-dev.md b/go-zero.dev/en/project-dev.md new file mode 100644 index 00000000..1f359fdf --- /dev/null +++ b/go-zero.dev/en/project-dev.md @@ -0,0 +1,35 @@ +# Project Development +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +In the previous chapters, we have introduced go-zero from the dimensions of some concepts, backgrounds, and quick start. Seeing this, I believe you already have some understanding of go-zero. +From here, we will start to explain the entire process from environment preparation to service deployment. In order to ensure that everyone can thoroughly understand the go-zero development process, then prepare your patience and move on. +In the chapters, the following subsections will be included: +* [Prepare](prepare.md) +* [Golang Installation](golang-install.md) +* [Go Module Configuration](gomod-config.md) +* [Goctl Installation](goctl-install.md) +* [protoc & protoc-gen-go Installation](protoc-install.md) +* [More](prepare-other.md) +* [Development Rules](dev-specification.md) + * [Naming Rules](naming-spec.md) + * [Route Rules](route-naming-spec.md) + * [Coding Rules](coding-spec.md) +* [Development Flow](dev-flow.md) +* [Configuration Introduction](config-introduction.md) + * [API Configuration](api-config.md) + * [RPC Configuration](rpc-config.md) +* [Business Development](business-dev.md) + * [Directory Structure](service-design.md) + * [Model Generation](model-gen.md) + * [API Coding](api-coding.md) + * [Business Coding](business-coding.md) + * [JWT](jwt.md) + * [Middleware](middleware.md) + * [RPC Implement & Call](rpc-call.md) + * [Error Handling](error-handle.md) +* [CI/CD](ci-cd.md) +* [Service Deployment](service-deployment.md) +* [Log Collection](log-collection.md) +* [Trace](trace.md) +* [Monitor](service-monitor.md) \ No newline at end of file diff --git a/go-zero.dev/en/protoc-install.md b/go-zero.dev/en/protoc-install.md new file mode 100644 index 00000000..ee74000b --- /dev/null +++ b/go-zero.dev/en/protoc-install.md @@ -0,0 +1,57 @@ +# protoc & protoc-gen-go安装 +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +## Forward +protoc is a tool written in C++, which can translate proto files into codes in the specified language. In the go-zero microservices, we use grpc to communicate between services, and the writing of grpc requires the use of protoc and the plug-in protoc-gen-go that translates into go language rpc stub code. + +Demonstration environment of this document +* mac OS +* protoc 3.14.0 + +## protoc installation + +* Enter the [protobuf release](https://github.com/protocolbuffers/protobuf/releases) page and select the compressed package file suitable for your operating system +* Unzip `protoc-3.14.0-osx-x86_64.zip` and enter `protoc-3.14.0-osx-x86_64` + ```shell + $ cd protoc-3.14.0-osx-x86_64/bin + ``` +* Move the started `protoc` binary file to any path added to the environment variable, such as `$GOPATH/bin`. It is not recommended putting it directly with the next path of the system. + ```shell + $ mv protoc $GOPATH/bin + ``` + > [!TIP] + > $GOPATH is the actual folder address of your local machine +* Verify the installation result + ```shell + $ protoc --version + ``` + ```shell + libprotoc 3.14.0 + ``` +## protoc-gen-* installation +With goctl versions greater than 1.2.1, there is no need to install the `protoc-gen-go` plugin, because after that version, goctl has been implemented as a plugin for `protoc`, and goctl will automatically +create a symbolic link `protoc-gen-goctl` to `goctl`, which will generate pb.go according to the following logic. +1. check if the `protoc-gen-goctl` plug-in exists in the environment variable, if so, skip to step 3 +2. detect the existence of `protoc-gen-go` plugin in the environment variable, if not, the generation process is finished +3. generate pb.go based on the detected plugins + +> [!TIPS] +> +> Windows may report an error, `A required privilege is not held by the client.`, because goctl needs to be run `as administrator` under Windows. +>The reason is that goctl needs to be run "as administrator" under Windows. +* Download and install `protoc-gen-go` + + If the goctl version is already 1.2.1 or later, you can ignore this step. + + ```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 + ``` +* Move protoc-gen-go to any path where environment variables are added, such as `$GOPATH/bin`, because the binary itself after `go get` is in the `$GOPATH/bin` directory, so just make sure your `$GOPATH/bin` can be in the environment variable. + +> **[!WARNING] +> protoc-gen-go installation failed, please read [Error](error.md) diff --git a/go-zero.dev/en/quick-start.md b/go-zero.dev/en/quick-start.md new file mode 100644 index 00000000..555f0892 --- /dev/null +++ b/go-zero.dev/en/quick-start.md @@ -0,0 +1,8 @@ +# Quick Start +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +This section mainly starts by quickly starting services such as api/rpc to let everyone have a macro concept of the project developed with go-zero, and we will introduce them in more detail in the follow-up. If you have already prepared the environment and tools with reference to [prepare](prepare.md), please follow the following sections to start the experience: + +* [monolithic service](monolithic-service.md) +* [micro service](micro-service.md) diff --git a/go-zero.dev/en/redis-cache.md b/go-zero.dev/en/redis-cache.md new file mode 100644 index 00000000..37462fc6 --- /dev/null +++ b/go-zero.dev/en/redis-cache.md @@ -0,0 +1,271 @@ +# Persistent layer cache +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +## Principles of Cache Design + +We only delete the cache without updating it. Once the data in the DB is modified, we will directly delete the corresponding cache instead of updating it. + +Let's see how the order of deleting the cache is correct. + +* Delete the cache first, then update the DB + +![redis-cache-01](./resource/redis-cache-01.png) + +Let's look at the situation of two concurrent requests. A request needs to update the data. The cache is deleted first, and then B requests to read the data. At this time, there is no data in the cache, and the data is loaded from the DB and written back to the cache, and then A updates the DB , Then the data in the cache will always be dirty data at this time, until the cache expires or there is a new data update request. As shown + +![redis-cache-02](./resource/redis-cache-02.png) + +* Update the DB first, then delete the cache + + ![redis-cache-03](./resource/redis-cache-03.png) + +A requests to update the DB first, and then B requests to read the data. At this time, the old data is returned. At this time, it can be considered that the A request has not been updated, and the final consistency is acceptable. Then A deletes the cache, and subsequent requests will Get the latest data, as shown in the figure +![redis-cache-04](./resource/redis-cache-04.png) + +Let's take another look at the normal request flow: + +* The first request to update the DB and delete the cache +* The second request to read the cache, if there is no data, read the data from the DB and write it back to the cache +* All subsequent read requests can be read directly from the cache + ![redis-cache-05](./resource/redis-cache-05.png) + +Let's take a look at the DB query, assuming that there are seven columns of ABCDEFG data in the row record: + +* A request to query only part of the column data, such as ABC, CDE or EFG in the request, as shown in the figure + ![redis-cache-06](./resource/redis-cache-06.png) + +* Query a single complete row record, as shown in the figure + ![redis-cache-07](./resource/redis-cache-07.png) + +* Query part or all of the columns of multiple rows, as shown in the figure + ![redis-cache-08](./resource/redis-cache-08.png) + +For the above three cases, firstly, we don’t need partial queries, because some queries cannot be cached. Once cached, the data is updated, and it is impossible to locate which data needs to be deleted; secondly, for multi-line queries, according to actual scenarios and If necessary, we will establish the corresponding mapping from the query conditions to the primary key in the business layer; and for the query of a single row of complete records, go-zero has a built-in complete cache management method. So the core principle is: **go-zero cache must be a complete line record**. + +Let's introduce in detail the cache processing methods of the three built-in scenarios in go-zero: + +* Cache based on primary key + ```text + PRIMARY KEY (`id`) + ``` + +This kind of cache is relatively the easiest to handle, just use the primary key as the key in redis to cache line records. + +* Cache based on unique index + ![redis-cache-09](./resource/redis-cache-09.webp) + +When doing index-based cache design, I used the design method of database index for reference. In database design, if you use the index to check data, the engine will first find the primary key in the tree of index -> primary key, and then use the primary key. To query row records, an indirect layer is introduced to solve the corresponding problem of index to row records. The same principle applies to the cache design of go-zero. + +Index-based cache is divided into single-column unique index and multi-column unique index: + +But for go-zero, single-column and multi-column are just different ways of generating cache keys, and the control logic behind them is the same. Then go-zero's built-in cache management can better control the data consistency problem, and also built-in to prevent the breakdown, penetration, and avalanche problems of the cache (these were discussed carefully when sharing at the gopherchina conference, see follow-up gopherchina Share video). + +In addition, go-zero has built-in cache access and access hit rate statistics, as shown below: + +```text +dbcache(sqlc) - qpm: 5057, hit_ratio: 99.7%, hit: 5044, miss: 13, db_fails: 0 +``` + +But for go-zero, single-column and multi-column are just different ways of generating cache keys, and the control logic behind them is the same. Then go-zero's built-in cache management can better control the data consistency problem, and also built-in to prevent the breakdown, penetration, and avalanche problems of the cache (these were discussed carefully when sharing at the gopherchina conference, see follow-up gopherchina Share video). + +* The single-column unique index is as follows: + ```text + UNIQUE KEY `product_idx` (`product`) + ``` + +* The multi-column unique index is as follows: + ```text + UNIQUE KEY `vendor_product_idx` (`vendor`, `product`) + ``` +## Cache code interpretation + +### 1. Cache logic based on the primary key +![redis-cache-10](./resource/redis-cache-10.png) + +The specific implementation code is as follows: +```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) + }) +} +``` + +The `Take` method here is to first get the data from the cache via the `key`, if you get it, return it directly, if you can't get it, then use the `query` method to go to the `DB` to read the complete row record and write it back Cache, and then return the data. The whole logic is relatively simple and easy to understand. + +Let's take a look at the implementation of `Take` in detail: +```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) + }) +} +``` + +The logic of `Take` is as follows: + +* Use key to find data from cache +* If found, return the data +* If you can't find it, use the query method to read the data +* After reading it, call c.SetCache(key, v) to set the cache + +The code and explanation of `doTake` are as follows: +```go +// v - The data object that needs to be read +// key - Cache key +// query - Method used to read complete data from DB +// cacheVal - Method used to write cache +func (c cacheNode) doTake(v interface{}, key string, query func(v interface{}) error, + cacheVal func(v interface{}) error) error { + // Use barriers to prevent cache breakdown and ensure that there is only one request in a process to load the data corresponding to the key + val, fresh, err := c.barrier.DoEx(key, func() (interface{}, error) { + // Read data from the cache + if err := c.doGetCache(key, v); err != nil { + // If it is a placeholder that was put in beforehand (to prevent cache penetration), then the default errNotFound is returned + // If it is an unknown error, then return directly, because we can't give up the cache error and directly send all requests to the DB, + // This will kill the DB in a high concurrency scenario + 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 + } + + // request DB + // If the returned error is errNotFound, then we need to set a placeholder in the cache to prevent the cache from penetrating + 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 { + // Statistics DB failed + c.stat.IncrementDbFails() + return nil, err + } + + // Write data to cache + if err = cacheVal(v); err != nil { + logx.Error(err) + } + } + + // Return json serialized data + 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() + + // Write data to the incoming v object + return jsonx.Unmarshal(val.([]byte), v) +} +``` + +### 2. Cache logic based on unique index +Because this block is more complicated, I used different colors to mark out the code block and logic of the response. `block 2` is actually the same as the cache based on the primary key. Here, I mainly talk about the logic of `block 1`. +![redis-cache-11](./resource/redis-cache-11.webp) + +The block 1 part of the code block is divided into two cases: + +* The primary key can be found from the cache through the index. At this time, the primary key is used directly to walk the logic of `block 2`, and the follow-up is the same as the above-based primary key-based caching logic. + +* The primary key cannot be found in the cache through the index + * Query the complete row record from the DB through the index, if there is an error, return + * After the complete row record is found, the cache of the primary key to the complete row record and the cache of the index to the primary key will be written to `redis` at the same time + * Return the required row data + +```go +// v-the data object that needs to be read +// key-cache key generated by index +// keyer-Use the primary key to generate a key based on the primary key cache +// indexQuery-method to read complete data from DB using index, need to return the primary key +// primaryQuery-method to get complete data from DB with primary key +func (cc CachedConn) QueryRowIndex(v interface{}, key string, keyer func(primary interface{}) string, + indexQuery IndexQueryFn, primaryQuery PrimaryQueryFn) error { + var primaryKey interface{} + var found bool + + // First query the cache through the index to see if there is a cache from the index to the primary key + if err := cc.cache.TakeWithExpire(&primaryKey, key, func(val interface{}, expire time.Duration) (err error) { + // If there is no cache of the index to the primary key, then the complete data is queried through the index + primaryKey, err = indexQuery(cc.db, v) + if err != nil { + return + } + + // The complete data is queried through the index, set to “found” and used directly later, no need to read data from the cache anymore + found = true + // Save the mapping from the primary key to the complete data in the cache. The TakeWithExpire method has saved the mapping from the index to the primary key in the cache. + return cc.cache.SetCacheWithExpire(keyer(primaryKey), v, expire+cacheSafeGapBetweenIndexAndPrimary) + }); err != nil { + return err + } + + // The data has been found through the index, just return directly + if found { + return nil + } + + // Read data from the cache through the primary key, if the cache is not available, read from the DB through the primaryQuery method and write back to the cache and then return the data + return cc.cache.Take(v, keyer(primaryKey), func(v interface{}) error { + return primaryQuery(cc.db, v, primaryKey) + }) +} +``` + +Let's look at a practical example +```go +func (m *defaultUserModel) FindOneByUser(user string) (*User, error) { + var resp User + // Generate index-based keys + indexKey := fmt.Sprintf("%s%v", cacheUserPrefix, user) + + err := m.QueryRowIndex(&resp, indexKey, + // Generate a complete data cache key based on the primary key + func(primary interface{}) string { + return fmt.Sprintf("user#%v", primary) + }, + // Index-based DB query method + 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 query method based on primary key + 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) + }) + + // Error handling, you need to determine whether the returned sqlc.ErrNotFound is, if it is, we use the ErrNotFound defined in this package to return + // Prevent users from perceiving whether or not the cache is used, and at the same time isolate the underlying dependencies + switch err { + case nil: + return &resp, nil + case sqlc.ErrNotFound: + return nil, ErrNotFound + default: + return nil, err + } +} +``` + +All the above cache automatic management codes can be automatically generated through [goctl](goctl.md), and the internal `CRUD` and cache of our team are basically automatically generated through [goctl](goctl.md), which can save A lot of development time, and the cache code itself is also very error-prone. Even with good code experience, it is difficult to write it correctly every time. Therefore, we recommend using automatic cache code generation tools as much as possible to avoid errors. + +# Guess you wants +* [The fourth phase-how to design go-zero cache in OpenTalk](https://www.bilibili.com/video/BV1Jy4y127Xu) +* [Goctl](goctl.md) \ No newline at end of file diff --git a/go-zero.dev/en/redis-lock.md b/go-zero.dev/en/redis-lock.md new file mode 100644 index 00000000..ac13b37b --- /dev/null +++ b/go-zero.dev/en/redis-lock.md @@ -0,0 +1,141 @@ +# redis lock +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +Since it is a lock, the first function that comes to mind is: **Anti-repeated clicks, only one request has an effect at a time**. + + +Since it is `redis`, it must be exclusive and also have some common features of locks: + + +- High performance +- No deadlock +- No lock failure after the node is down + + + +In `go-zero`, redis `set key nx` can be used to ensure that the write is successful when the key does not exist. `px` can automatically delete the key after the timeout. "The worst case is that the key is automatically deleted after the timeout, so that there will be no death. lock" + + +## example + + +```go +redisLockKey := fmt.Sprintf("%v%v", redisTpl, headId) +// 1. New redislock +redisLock := redis.NewRedisLock(redisConn, redisLockKey) +// 2. Optional operation, set the redislock expiration time +redisLock.SetExpire(redisLockExpireSeconds) +if ok, err := redisLock.Acquire(); !ok || err != nil { + return nil, errors.New("another user is currently operating, please try again later") +} +defer func() { + recover() + redisLock.Release() +}() +``` + + +It is the same as when you use `sync.Mutex`. Lock and unlock, perform your business operations. + + +## Acquire the lock + + +```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 + } +} +``` + + +First introduce several `redis` command options, the following are the added options for the `set` command: + + +- `ex seconds` : Set the key expiration time, in s +- `px milliseconds` : set the key expiration time in milliseconds +- `nx` : When the key does not exist, set the value of the key +- `xx` : When the key exists, the value of the key will be set + + + +The input parameters involved in `lua script`: + + + +| args | example | description | +| --- | --- | --- | +| KEYS[1] | key$20201026 | redis key | +| ARGV[1] | lmnopqrstuvwxyzABCD | Unique ID: random string | +| ARGV[2] | 30000 | Set the expiration time of the lock | + + + +Then talk about the code features: + + +1. The `Lua` script guarantees atomicity "Of course, multiple operations are implemented as one operation in Redis, that is, a single command operation" +1. Use `set key value px milliseconds nx` +1. `value` is unique +1. When locking, first determine whether the `value` of the `key` is consistent with the previous setting, and modify the expiration time if it is consistent + + + +## Release lock + + +```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 + } +} +``` + + +You only need to pay attention to one point when releasing the lock: + + +**Can't release other people's locks, can't release other people's locks, can't release other people's locks** + + +Therefore, you need to first `get(key) == value「key」`, and then go to `delete` if it is true diff --git a/go-zero.dev/en/resource/3aefec98-56eb-45a6-a4b2-9adbdf4d63c0.png b/go-zero.dev/en/resource/3aefec98-56eb-45a6-a4b2-9adbdf4d63c0.png new file mode 100644 index 00000000..603f8a97 Binary files /dev/null and b/go-zero.dev/en/resource/3aefec98-56eb-45a6-a4b2-9adbdf4d63c0.png differ diff --git a/go-zero.dev/en/resource/3bbddc1ebb79455da91dfcf3da6bc72f_tplv-k3u1fbpfcp-zoom-1.image.png b/go-zero.dev/en/resource/3bbddc1ebb79455da91dfcf3da6bc72f_tplv-k3u1fbpfcp-zoom-1.image.png new file mode 100644 index 00000000..f530f572 Binary files /dev/null and b/go-zero.dev/en/resource/3bbddc1ebb79455da91dfcf3da6bc72f_tplv-k3u1fbpfcp-zoom-1.image.png differ diff --git a/go-zero.dev/en/resource/76108cc071154e2faa66eada81857fb0_tplv-k3u1fbpfcp-zoom-1.image.png b/go-zero.dev/en/resource/76108cc071154e2faa66eada81857fb0_tplv-k3u1fbpfcp-zoom-1.image.png new file mode 100644 index 00000000..42bdbd1c Binary files /dev/null and b/go-zero.dev/en/resource/76108cc071154e2faa66eada81857fb0_tplv-k3u1fbpfcp-zoom-1.image.png differ diff --git a/go-zero.dev/en/resource/7715f4b6-8739-41ac-8c8c-04d187172e9d.png b/go-zero.dev/en/resource/7715f4b6-8739-41ac-8c8c-04d187172e9d.png new file mode 100644 index 00000000..4513c5ac Binary files /dev/null and b/go-zero.dev/en/resource/7715f4b6-8739-41ac-8c8c-04d187172e9d.png differ diff --git a/go-zero.dev/en/resource/7e0fd2b8-d4c1-4130-a216-a7d3d4301116.png b/go-zero.dev/en/resource/7e0fd2b8-d4c1-4130-a216-a7d3d4301116.png new file mode 100644 index 00000000..37e6fe93 Binary files /dev/null and b/go-zero.dev/en/resource/7e0fd2b8-d4c1-4130-a216-a7d3d4301116.png differ diff --git a/go-zero.dev/en/resource/alert.png b/go-zero.dev/en/resource/alert.png new file mode 100644 index 00000000..f24580d3 Binary files /dev/null and b/go-zero.dev/en/resource/alert.png differ diff --git a/go-zero.dev/en/resource/api-compare.png b/go-zero.dev/en/resource/api-compare.png new file mode 100644 index 00000000..eb083a0a Binary files /dev/null and b/go-zero.dev/en/resource/api-compare.png differ diff --git a/go-zero.dev/en/resource/api-new.png b/go-zero.dev/en/resource/api-new.png new file mode 100644 index 00000000..469a8cd0 Binary files /dev/null and b/go-zero.dev/en/resource/api-new.png differ diff --git a/go-zero.dev/en/resource/architechture.svg b/go-zero.dev/en/resource/architechture.svg new file mode 100644 index 00000000..161e1262 --- /dev/null +++ b/go-zero.dev/en/resource/architechture.svg @@ -0,0 +1,16 @@ + + + eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO19WXNcIlmy5nv/irS6ZvdpiD770m+IVSBcdTAwMTCLXHUwMDEwXHUwMDEy02NcdTAwMThLsO87jM1/XHUwMDFmP8qsVISIYFx1MDAxMaBcZrVduqsqU3FcdTAwMDSHXGL3z5fj/vn//cePXHUwMDFmfy22XHUwMDEz+69//fjL3jRqg25zVlv/9b/Mz1f2bN5cdTAwMWSP4Fx1MDAxMnn7+3y8nDXeVnZcdTAwMTaLyfxf//zn+29YjfHw52/ZXHUwMDAze2iPXHUwMDE2c1j3v+HvP37837d/Oz6nNpuNf37E24/fP1x1MDAwNiOiP/44O1x1MDAxZb19JlxcI1RzLMXvXHUwMDE13XlcdTAwMTQ+bGE34XKrNpjb71fMj/7KVUQ0XHUwMDFkrSZ4tbqj6ddwPNpZZt8/t9VcdTAwMWRcZoqL7eBtT/MxfI/3a/PFbNy3y93movP313f83O+3ZuNluzOy53PX74wntUZ3sX37XHUwMDBl6PdPa6P223u8/2RcdTAwMDN/05xbSlKJkSaIcEF/XzW/XHUwMDFmwohbXHUwMDAyIVwiseJcXCHn7fq5s8h4MJ6Znf1cdTAwMTd6e73vrV5r9NuwwVHzfU3r7fW+Zv3r+2JFLKqUYIJphjXB79vo2N12Z2F2aiF4XHUwMDFhWiFOmNCMvz+Wuf32RFxiRkxJ/H5cdTAwMWbMp0/um2+C8X8+3tFObTb5def+etulY+fmr7GfUuXx67XZ4q47anZHbbjyt6y9y+H9myi06lx1MDAxYpTIZ+nkcbF6mW5cdTAwMTNTXFx4XHUwMDE4/P5cYrO52sR8cea8+5xr7ljSXHUwMDFhN5bm00PI4oooxVxiIUJgpX4t+X+/N2WPmse3hLZcdTAwMWRcXFxc2OtSrVB9biSeWzTWXHUwMDFl72+JXGJcdTAwMGJ2JFx1MDAxOOHwyDnGWnhsXHRZhDOFXHUwMDA0UppcdTAwMGLOkZR7e5qMu06lNK/3P/14XHUwMDE3w7e//P7z//lfnqv9XHUwMDA1xLz2ROP97f7x4W3/XHUwMDFh1OaLyHg47C5Aj3Nmj7DF0XIwcD/hsIGMjl1rfrxcbrfaee1cdTAwMTe2/MPx3c+BXHUwMDFmiv3gR2J45qB66mT0ybZcdTAwMTOd0GqzXHUwMDBlt1fzYb37zFx1MDAxZUqvoWCjXHUwMDBmxlRYiIJgI05cdMXaXHI/gE2actBrhSShlIlcdTAwMGZcdTAwMWK7XHUwMDEy+EhsgeTAXHUwMDA3KKKVkI5b/lx1MDAxYnyQpVx1MDAwNcg5XHUwMDE2kmIsXHUwMDE5l/Ij+mCuXHTslIv33/6T+HOqsiv4YvBtNFx1MDAxM1IosHg++IOUIFxicVA/RSVcdTAwMTH4b1xyO1x1MDAxM4JOREVCLa3w27MgjFx1MDAxYkXwhiCGjZFcdTAwMDZAQFx1MDAxYVx0qm9cdTAwMGJBIX85efv9PVx0+S4gxFx1MDAxY1x1MDAxZc5cdTAwMDdcdTAwMTBcdTAwMTKEU8pcdTAwMTU7XHUwMDFkhMbIfrgvb+eNymM/223k0rJeXHUwMDBmuFx1MDAwYlx1MDAxNFLaXHUwMDAyXHUwMDA00lx1MDAxY2GmXHUwMDA0JuJdt998IM0tJLFcdTAwMDbhx4RJwKmboFx1MDAxMKiXpTTgIXhcdTAwMDJcYmT//VNcdTAwMWMgxKlcdTAwMDBF1IiC+DHQiz1cdTAwMTBcdTAwMDJ7jEBcdTAwMDRcdTAwMDPiXHUwMDA0abv3vNDzeqS8WoTqXHUwMDFi+1x1MDAxZVx0drev7lx1MDAxNL65olxcI4JcdTAwMThlzM9cdTAwMDeilEr4v1x1MDAwNK9cdTAwMDQhyiX/XHUwMDFjXHUwMDA2dVwir6uHfr1eXHUwMDFhtKd3NNSLLcIqu78pXHUwMDBlN1x1MDAxYu4w0kwxkFx1MDAwYlwiPTZcdTAwMDV7QpSCwHDM4LHB6lx1MDAxYrtBvkJiXqF9+fguXHUwMDEwxJEvXHUwMDA0cfAvXHUwMDAxhsi7zlx1MDAxZYOgZSw8z8Rj62ey7WeTu4dlJT99XHI2XHUwMDA0KWFcdTAwMTHwuVx1MDAwMYJcdTAwMTgosCO0Mb+uLC5cdTAwMDGEMVx1MDAxOEKw0c5bdV0vSFpcdTAwMWMr45IqXHJwRzxCMGpcdTAwMTlhp4IzXHRcdTAwMWW4XHUwMDA2r2FcdTAwMWaAmMaAlO9Bw1x1MDAxZlx1MDAwNaBcdTAwMTN1XHUwMDFkXHUwMDAwSFxuQmHvXHUwMDFjXHUwMDFihcdcdTAwMWW6XHUwMDBl2oWN7adcdTAwMDRshOJcdTAwMTCyfVx1MDAxNoBOREVcdTAwMDJRXHJ4Wlx1MDAxNLxKglx1MDAxOUNeqGj2XHUwMDA04KMpOJ5cbiw1u7lcdTAwMGLkJyTmtS9cdTAwMWXBwp+Z3Vj81D9cdTAwMGZcZlx1MDAxMlx1MDAwZWv6XHUwMDAxgoimXHUwMDFh/KAzIOjwI3ZDUGM2ns9Dndqi0fnzQFx1MDAxNGImI4HgXHUwMDExgoVcdTAwMDWXSIrfYvfTXHUwMDE5XHUwMDAyJLaoRFhcdFx1MDAxM4dcdTAwMGKGL0MjXHUwMDFi4nmFPLwhzaz3T/5cckBM7iNcdTAwMGW4bVxcS0a9XZ53nVmP70k7XHUwMDE5f6qmx1Fa4C9RfV/o/bUnl59cdTAwMDKm9085bHx+XHUwMDFjc5BhQW+RL/BoXCJqN/S6qlx1MDAwN9XJjudd77CI6k4/q1ZolY0/i+FgUb9L9JxcdTAwMGJcbuv7XHUwMDE5zZTK2Vx1MDAwZVlcdTAwMTft2GyiUX7nXFzQ5eipxFc8LNuz1ON2wdexXFzm9604qENcdTAwMGJ7s/BUXHUwMDFmwvzUR2u4KKliJ6tcdTAwMGbEfmIgQ6+t+/64PJSZ8l1k3lxmtlx1MDAwNVx1MDAwZoH5Pqg4XHUwMDEwZHyF3lCPuFx1MDAwMTuztr9ypUhzjaVcdTAwMTboZlozN3850Zy3xqNFsbszt5VcbtdP47Vhd2Bu4Ls78iaC8Fx1MDAwMWW7/u9ReDJcdTAwMTl0XHUwMDFitVx1MDAwNcjbv0fx7sxe11x1MDAwNlx1MDAwM+e9m9uD7ugtn0+w6y3Cg2579Fx1MDAwNsCwI3vmXHUwMDEy6lx1MDAwNbzl4PeCxXhy2LDYg0F3Mvc0K1x1MDAxNPlcdTAwMWUwgGdrgkpxul7EW71cdTAwMTDK3d9ccnfJOetvxLg+57GA61x1MDAwNYR1lna+Plx1MDAwNNdY+Fxcvu75gkdOz/nDX2pBXHUwMDE5+HWcXHUwMDEz71x1MDAxY967VsRcdTAwMTkr8KRdS98tXiq5lL3u8lx1MDAxOb+SVrx/SiazK4UqnIXii1x1MDAxOClccqL9XGJHi1x1MDAxYuM44b44zsDl1UI4XHUwMDEw5pi8NuPT6GOp31x1MDAwZqHK/UuYL2OFTbJcdTAwMTN0eSXKYv7yqpjwuXxNeVx1MDAxNVx1MDAxZcFcdTAwMTejXHUwMDFmpZWDo821/qPSeiGGXHUwMDAzfHuCNeWupV+E1dT/MFgxXHTWm6jTM6HTTX9U2lx1MDAxNNr5tJrcv26KuU38MeiZUFxiIVx1MDAwZlx0f8DAXHUwMDFhQnFzPHRE+pNduzV5LYQ7ndHLaNN4JdHosnp1rL5cdTAwMWJ2npPDskDb+uAuXHUwMDFiXHUwMDFmTGeh+/aFUKylrzhcIvD+XHUwMDEwo/h0LO4/0S1f7ULZ1PghkajP21x1MDAwMkWmXHUwMDAxXHUwMDE3R1wiySHfIVBYrMFyYqGOYfEtpfFyfzpAWCxcdTAwMGVgsdBcdTAwMTJjfYbw28VMflx1MDAxMG5tsrF2r72hyylcdTAwMDQ1XHUwMDAxP1x1MDAxYVx1MDAwZik3XHUwMDE0qz0s9tONP4HFXHUwMDFjcVx1MDAwNFx1MDAxMSk7Jv7h52HlVc+fM/bUftLLVGlcdTAwMTGebK5cdTAwMGXG3lx0kIvAmFJfMOZCMaJcdTAwMDU/vU6Mj1x1MDAwN5VOKz1pxTv1XHUwMDExW/VaiXlhXHUwMDE2cHFcdTAwMDRxc4mjclx1MDAxZlJcdTAwMDBcdTAwMTiTL1x1MDAwMGPmkd7YXHUwMDA3Y4xcYjxcdTAwMTPkd1xm+jXieCFcdTAwMWHnXCJfXGbGvqdzQvlcdTAwMTYpXHUwMDExXHUwMDAxXHUwMDBlXHUwMDE4I+r0zLh3yFx1MDAxYWzRZ1x1MDAxYVuYYcJcdJGUKa0/pPYkVlx1MDAxNiFcdTAwMWNCMcxcYlx1MDAxNZjfRPQx51x1MDAxNpZcbj5HXHRFXHUwMDE5I175cfDfOUg/XHUwMDAzXHUwMDFiiaSp3dhTXHL4TcpcdTAwMTH821M1vvqA7nBG64er2lx1MDAwN76TXHUwMDAwj9+cLiFMhfNA/lcxJbI0VUiCe6CkKcfaP42/zlx1MDAwMZ3j+Fxyc6VcdTAwMDH9JVx1MDAxN1xcwlx1MDAxZN/bXHUwMDEzRVx1MDAxNmVcdTAwMWNLXHUwMDA0waVgVN+6TtJXSMxrXzze329cdTAwMGb0PnFA91x1MDAxMUOuWSMgXHUwMDFkevrRITRCzZg6Izj3XHUwMDBl14JcckOEaUtoXHUwMDBlos1cdTAwMDFEXGKS71/3XHUwMDE3XGZxi4Kogu2VRCt+I4/QXHUwMDE0mXBcdTAwMDFcIm+KwkFcdTAwMTM9UIhgi3GhsUBaapD5vWJJSTVSRPic2X01XGJcdTAwMWTO1Lj0XHUwMDFkUbj/XHUwMDAyU1OUKCk8g31cdTAwMTBiXHUwMDE2MkVcdTAwMTBcbnFYJpQ4XHUwMDAyQi6NuvJpvd+jMq/9h/R9sEBI3yQ1wYhcbqaIPt1cdTAwMWI/fFxcXHUwMDFhTCygXHUwMDE0TJ1cdTAwMDY0oODlgrazXHUwMDBmUEDA7IAzXCI5UYLJW3kkXHUwMDAwSFx1MDAwNFx1MDAxNFx1MDAwMlx1MDAxY1wiU1x1MDAwNke9YkVGpcUoVljDTijT+746w1x1MDAxNPapXHUwMDAyUjF0OFPgxlx1MDAwMtizqbyR2DxcYinxPlx1MDAxNlCLXHUwMDExMPrElCiA88I+2blxskNiKrUpgrvNMGKSS1OrQ/a2XHUwMDA1Plx0x1x1MDAwNHx3ZKrKpFb0tk5JyF9QzGtPRFx1MDAwMlx1MDAwN0RcdTAwMDfyVFx1MDAwN87LMLhfXHUwMDAyIU5Ph6LMa+shK1x1MDAxMK7Xas+hSaKuQopcdTAwMTZcdTAwMDJcdTAwMGVFXHUwMDFjXHUwMDFl34eAiEvqyk2pXHUwMDBmuzlcdTAwMGZ+XHUwMDE0arVse1x1MDAxZn649MpN0b3clCamLN9cdTAwMTmkeidcdTAwMDPaj2zXmFx1MDAwZWU1PX1cdTAwMWRss+Fa4SXBrpVcZjgoX35JJ0fv1b5wmYBcdTAwMDM8kNN93sf+oNmayGyOqXxyWFxuzZ66637Ahcv51H6KXHUwMDE2cef8byNap+X8MTdOLNHIO5b+XCLJen+An0kzRaJZ5y36gjxTs1tcdTAwMWKOnWbb2YqC/FNNXHUwMDE4LC1cdTAwMDSBZ1xi/CxR2+ldP1x1MDAxYr/f6fB82HihvFZcdTAwMGa2wFOMsUWZgi9cboJFXHUwMDE5cjfEmVx1MDAxMJBA/GFqlVx1MDAwNVx1MDAwNIL4XCLp9/PrnJWuv8Wf4L2GWy6FaTfA3p7bu/jzdqJVx7VhPrZ+KKXUZD24u0tcXD3pz1uv5UhJJsLbWm2ZfI735st63OkkLfOFxiBcdTAwMTmuoFSulY3rVam1pEXngs620Fx1MDAxOO1cdTAwMWU6eN2NvFx1MDAxNO5cdTAwMTRb7SpR51x1MDAwMp1ehzPbhZxkXHUwMDFm6cCeZ8KdNY04XHUwMDE33Fx1MDAwZvK76qrCc8Viqsln05f068LV33ZcdTAwMWZNrCqJxyiZdObxULi9KNrSdi6IpcIvKZ18LT6GJ5Mxybwmm+GN61s0qslyWuR1Kk0jjWahmI09XHUwMDBlnVx1MDAwYlQnftdoytRuycMqwnG9PXp2RbP5cMjeKdVtz6fbqp1cdTAwMGatey1du+x0XHUwMDA0XFx46qe4WGDTT6zOUFxcXHUwMDFhm1VY4+4+VKrHJ4/D12qz8LRcYrriKmUpX8VcdTAwMDWv15JcdTAwMGXFvU085jgzPWC2IESWiPxZtb3QakXt1Y8nuzb8YtN1sIFcdTAwMDBcdTAwMTPse0QoyM+M9OkqkI6mysNaJlx1MDAxY3voduO9gYpOXHUwMDFiLOAqXHUwMDAwISV2uf1cdTAwMWZbXHUwMDA31DVPrGuk3mjhfSVQmvp8ym+VcHZ4/9JcdGX8aVO1fUQnhsNEuj8vXHUwMDBmmtv2y7xWfO50Vjh9dVO2qaVjVFRcdTAwMDdF8jSfglx1MDAxYcZe++7m7UpqlERcdTAwMGJRiUSTXHUwMDA1SauF6lNq6VpQKvO78aZHc612UUlcdTAwMTFrjJv00WUlcvHpmG3T60mxUHhpNlQzLMLOXHUwMDA1jajejMkwmmo2XkdcdTAwMDX01MvdtcquXHUwMDA1XHUwMDA11E2XNuv7ev41k6HVUdkuz5xcdTAwMGJESE1ym/A6kVtcdTAwMGVTk5fVulWOXHJOszOHXHUwMDE1XHJefoomwVx1MDAxZNGCnFFTXaqhxa6e3yV3y3ZcItqtbcWTrFx1MDAwNFxc0fDHfFx1MDAxZkXXLMy7rmphKiFKpVhcdTAwMWY7jFx1MDAwZrRuXS62ztOOjy5cdTAwMTJRhmTnnNjmML9DQMVWMJ+00E8hpn6dXHUwMDAyXHUwMDAxXHUwMDEwYk2kRlxcqmOhfqCF2LHgMFXVj2NsMrBcdTAwMDBcdTAwMTW6XXsyfVx1MDAxYcZmL53MpFWMlVnCuSCc3FTW/XlcIorvandTXq/j6qDpXFxQK61cdTAwMDbLVS+cQm2b3z+haCtSdS0oJyvieVx1MDAxZLtvTJelxuxxx0ey5+p0SyXrq00hvtCzot7hzCqyXi+Ic1x1MDAwMV2ti5VcdTAwMDXh43A+V5o0mpNEqEGdXHUwMDBi2l3aQ+mEYstYvstcdO5cdTAwMTWbxJVar5ZcdTAwMWHrJO2vas+b8rCu8pknXHUwMDEyXV1YS8b9kcA0+mqNzqiooVx1MDAxNbs6yIZ76Vx1MDAxN/J0n8BrUomna1x1MDAwMUdcdTAwMDJnh81bXkNJi6P312V5XHKN68grq0ew12H1XnwkXHTl7n7Fr1f1XHUwMDBi46P0sm7PRiA087NcIqSB3XJcdTAwMGLs9ep5NfJtr8ZMI9Nf4XBcdTAwMTCOyXxSXHUwMDE2m6m7QSfZj61T6dhDcjwqPfjI/I36q/HnJJ9Qd1QkuIWcgn9xRs8jLaA9iiadP/xb7oWiUuqjflpYNYr2w53qd0KdQbz+tC5kxl0nZHZbjXE53ExURXLTXG7zaXv7tNTOXHUwMDA1X2MkcyNRa9/nWo18rLxcdTAwMTjNXG7PhcjEZaDsdv+Jzzjtc4Gf51x1MDAxMTqLjWJTl1x1MDAxNX2dXHUwMDE2XHUwMDEy0YdIsZTb8cR96kHmOsxlXHUwMDAza5mBRJGX9eYlXHUwMDFhXG7Hy5XWY8yVc1xmZ+XLiN2/rDPTXCJcdTAwMWSxca3OUNK5oFhJXHUwMDE07JF6XHUwMDFhp+x1r5y5i91Ppugy86KIb3jEtYSrhJxcdTAwMWVcdTAwMWWNXjvRwd02dMdXvVx1MDAwZU9cdTAwMTXqz4859S00jTqsyNuvUHZFXHUwMDFic0GrXGLRmjPDnvit9ezSxr/c/XWTd8Nus1x0YvBZ+4TRXHUwMDAxXHUwMDAzZSoliKst+JjarDas+zBNNKdykY+FWqpRy1x1MDAwZfzSd8FSXHUwMDFiwd1qI+hcdTAwMDdcdTAwMDPFrq83p1ooTFx1MDAxNVx1MDAxMkzyo6xcdTAwMDW7XFxoXVx1MDAxM7Fpt1wi16tWhfae7kLxwKjOl1ogPKfwXHUwMDE1XHUwMDFhvW3nha9cdTAwMWHNeao/und9RLtde1xmr56eu1x1MDAwZolcdTAwMWVcdTAwMTkl5aKbeKg5XHUwMDE3vK7iXHUwMDFinkLTtF2flu95e1x1MDAxMFx1MDAxYlx1MDAxNCfOXHUwMDA1T9thYjSNZdiuuiyXVt1x+4WVLrRh2teGYUyEIFieUVazq4dr3U5fkucoXHTniyzRy65cdTAwMTPfQlx1MDAxYiVx1z9gSoJhxDCXjFPMxLEoKdiqeKFcdTAwMTUrXFy7S+dcdTAwMTQrdoDJU/nWXHUwMDBiXHUwMDExhTVgOzrd8ztcZj1BVlx1MDAxYUbcvZKYyJtcdTAwMDdZxKtmXHUwMDAyoz21UUgqwzV3LMhcbohWnFg1ezhcdTAwMWX/4SpQZfBcdTAwMDA4h4egXHUwMDE4XHUwMDE4clx1MDAwN4Hcr+pUaUmKXHUwMDE41opqjDUnv66fWTJ72Fx1MDAwMfvh6uFRxFSyUCEwYqZqf29LzGJUI1xuj82QJslcdTAwMWKz7DlEybyw41x1MDAxN/ZcdTAwMWVvoFg8XHUwMDE59ycs0NQ0QakzmmRcdTAwMGZ7NkHGn48uNNZcdTAwMWaSPDeAXHUwMDFmRzXHe8mWXHUwMDE3+kilj7N+fS/0OVnXXHKJsGKEXHUwMDExqVx1MDAxNEThlO1purAgykOMaUZccssl/2RcdTAwMDfhyXhcYlx1MDAxYlx1MDAwMluglMDwadrM1/DoKFwilsJYXG6GhDY8yfTGPYQhhzD9/HuwXHUwMDAw6FBcdTAwMTSPsX9cdTAwMWSamVx1MDAxZiE0kaenmcfliN1ev5R2tYd+L5foqqQmL99cdTAwMDKCXHUwMDE4c9efXHUwMDFkoFL5+iheKzBcdTAwMDQnJJqnoVx1MDAwMdtu1cM0ukwwO7otLlx1MDAxOEpcdTAwMDUmdHCofmDzyI5cdTAwMDWfzFx1MDAwM/x9r1x1MDAwZWqkb1koYr5cdTAwMWVcdTAwMDGYIVx1MDAwNa4gO/2gM52JzneZVnrJ6Gqk2K5MOzz8PbRR77XIXHUwMDEwRK9Ll+FR6cD21XE/kDfNwlx1MDAwNIzdscq3YCvjhXF87ClcdTAwMTJ13uE/XHUwMDFkyPNcdTAwMDNcdTAwMTVcdTAwMDJYMmqmnZxuxlx1MDAwZYNHoFx1MDAxNYcqXHUwMDBiLFx1MDAxN1bSzIHB4lx1MDAwM+dcZmbYeiv3Q1xu7olkXHUwMDBl4389q4aUpVx1MDAxNYR9XFxLXHUwMDEzuztI2ZyESJZJrzCJYa+aeJg8wVx1MDAwNGL8KFldQJToRMf7sH/kcnPhIVwigbjmXGJcdNOxvO/lXCLLlFx1MDAwMZiZPoZGhexP87mu51xysVx1MDAwMMT7gmhcdTAwMDZcdTAwMDG9oczw2JKyXHUwMDEwPHdGONJcdTAwMWHcR49cXMSV+2R9Zc289qQsWG75XHUwMDAxNJOHurqI5mc55Yc9nSCjXHUwMDE5XHUwMDA1N1x1MDAwMEFcdTAwMTRcIikynDDcPWeIXHUwMDFidlx1MDAwZlx1MDAwNlx1MDAwZpZcdIZcdFGXN+17uFx1MDAwNNhCXGJglIPB18yr9lx1MDAxMXNmmVx1MDAxMFOCuCNq6J32sFxmlJNRSfgxj+F7YdlcdTAwMTkhO8aGXHUwMDAzxNDrSCwodc5cdTAwMDH7e3ZcdTAwMDezNITyXHUwMDE0bjVcdTAwMDF9XHUwMDE1+HNgdlx1MDAwNr7CRiQ2XHUwMDE0VVx1MDAxY0khpd7bkrBcdTAwMTg8cFx1MDAwNZdcdTAwMDA6uML7VFx1MDAwNFfFMj9JM6/QvpB9XHUwMDFiKGO+VVx1MDAwMlx1MDAwNFx0ZXI06PSI5nBMXHUwMDE2ZChcdTAwMTNcdTAwMTC+UI1cYohcdTAwMTJiguBcdTAwMGb9XGKamiQ+PH9EuDFYN4hupLZcZlpcblxy4s6lRJ5YRsFgaqTM+Fx1MDAxNka1VyqCg1x1MDAxYfPjJMLfXHUwMDBiy07GXHIz1EhTKt5cdTAwMWXWzymb+26QtohcdTAwMDBjZbiWuXG1P+mZnZOlhb1QeC7gXHQxcL1cdTAwMTDdT4pcdTAwMTJiYU6YOZfBPzlcYm+LZ77iZl77gvZt8Ez6XHUwMDA3moJzSaU6g0npcClJkPFMXHUwMDFhTlx1MDAxY62EgsiaXHUwMDE598uFZ1xmgfFcdTAwMTTgk3FcdTAwMGVxXHUwMDAyxFx1MDAwYjdwzTSxXHUwMDA0MeONwfOnRFwir1xcqpCWIbdScNmQmnhcZqElXGJ2yTT9z/LNzjjMZVxmvr7xvaTQXHUwMDEwcjpPMt6Hy1xuRbTptKBcdTAwMTDesU/C2TlcdTAwMThrhlZcdTAwMWIuIFx1MDAxM29ij/NlbNxFQ1x1MDAxMGkwREOYfOtA01fY3q7uy1mw4OzgXHUwMDAxXHUwMDEw9T9cdTAwMDBcIsTQVJMzXHUwMDFjtHRcdTAwMTGlZ502203sUun+OTyy7ZLfKMlgXHUwMDAxmsJ7XHUwMDA3QCowXHUwMDA3QEwhsJJH2ZmX28KgIzarLVm+lpvJXHUwMDE5jldw36li/3P+83vBrc5/XHUwMDFjXHUwMDBi/kRcdTAwMWSoOVx1MDAxNvfXZ6WYJOqM3FEuNWyLZrpf1fh+OVwiT8nCa/R71JSoj2yvXHUwMDA06VtcdTAwMWYgnVZcdFxuPlwiXHUwMDA1p+Po5Itga/OFXHUwMDA3SKnxlWdnXFx0fiSl77Er44RrpM8onj6s9kHWXHUwMDE5XHUwMDA3JedPN/5jr91cclpcdTAwMTmkl1x0pPsmkGhBKWHkWPl0QJTiXHUwMDE2lVhMY1x1MDAwMVx1MDAxZTg23VCKeYT44M2bZKXhTlx1MDAxN1x1MDAxMHp90lE/7MO5glx1MDAwN0yYRGaws9BmlKfep3M/vqfrZlx1MDAxZD445vS7JFx1MDAxNqT/kY+AXHUwMDFiS5DzKPRcdTAwMThcdTAwMDJ5+1x1MDAxNd9cdTAwMDGB1N6oXGI3xYW6RWLUo+xcdTAwMDOzPVx1MDAwNOKmXGKdOZ2n/1x1MDAwNFx1MDAwMDpZ2d9cbtG1olx1MDAwMmvEOFx1MDAxMpLsn+JgYSmJTXGBXHUwMDE0ZlauR+H3tWvRwThrXHUwMDBlz1x1MDAwNSmwT8yDUVx1MDAxYYI4M95BKVPiZzZ2W1x1MDAwMFxuSeZcXFx1MDAwZnJcdTAwMTQsXHUwMDAwOpBcblD+LFGYXHUwMDFhxlx1MDAxZK7OXHUwMDE4ZZp4TFx1MDAwZcdcdTAwMGbDUTdcdTAwMWaq9Fx1MDAxYZVdY9Tf6K/FoM9xbXAqLIZcdTAwMDUnZkiG+lBBwzlcdTAwMTg9qkxfg1x1MDAxOVxirS6rnyFMXHUwMDBiu+lcdTAwMTFHXHUwMDEwXHUwMDBmomZC90e5m5MkXG6CfSyUeFx1MDAxY1x1MDAxNnfV1ch+XHUwMDFjJ8piXHUwMDE5e+zKl3n26mH9Uf6aeKuWWWRmy071wZ5Ntjry3N25XCJmOery1ehxoqKVynqyKNj97bOLcTS0a5eScvM0XHUwMDBi3Zc7K3s+rk86XHUwMDE3ksso4l9yXHR3VzFyhtS/XHUwMDBlXG4xsWqEnmVnWX+N1CaDSC/1XHUwMDFkpF5oZmmH1Lvz+cLUjd1cXOypV0+GJ4s0l+h4/HxLob+UjnPc6NuzXHUwMDFmXHUwMDA1u92Fj99eN1x1MDAxMv784DLMlW/yXGJcXFDEsFx1MDAxNKe39Fx1MDAxZoaDQPIsaUEsRZhQXG50XHUwMDAwJP1cdTAwMDP4XHUwMDFivjVGTZtcdTAwMWRcdTAwMDUhdJ4mXZOUVitcdTAwMGJJxTGWXG5Jqr2SSqCLXHUwMDE2M2Ur4NNIeDJ63zRAMEaFi+7+T05cdDnsXHK43UzAWy2Mm1x0qMMk0/tcdTAwMDfsRFqEcaqlXHUwMDA2t05BVPnJrqPDnIDuXSHTUiQ5NoNcdTAwMWPN8blH9SN3hSri1rGuv6CYV2hfRoLliVx1MDAxZeR+lMq3bMjcWsHZXHUwMDE5p+z5Z9LY1nr8ZUCLyVYn+tTK01xcsIFcYiwttrgkXHUwMDE4glx1MDAxN5A8rt2lj1x1MDAxOGNmcSawYVx0xVhfWMbd1DWh9pGIIc95Ifs92uA0UM5POJ96ZFx1MDAxNVbOb3S+cl9nzeog0cN1l75cdTAwMTXym/7DQ0f0krmcqsvibIa31yeDzORik9W8kGHpzFOmfKd3i1HqXG6EpcoxYXKveedtwOA5I29cdTAwMDfrViZUotNNLqzW69xWV+/XQVx1MDAxN1pKXHUwMDBmXHQtXHUwMDE41OBcYi1DTFJcdTAwMTevYqBltprapVhkXY3T6cNcdTAwMTNcdTAwMWWKu1x1MDAxN44215BZ/1x1MDAxNnSCiCT6LN6YVCVcXL2fxCpcdTAwMTk+ypLBsFx1MDAxNUq17lx1MDAwM85cck0oPiy0MkBCK8D2MXV8fk5AhLaQ1OhpcV9cdTAwMWKAOLyun3Kvj2PpXCJV95Zqx4JIp1x1MDAxZEpnX2r5tL0pJMeD5K6KXFys6z1VtOX4pVmN7VaDzNPrbFx1MDAxYu27OFx1MDAwYtfDeD1UScZkb5dJjMJN/VRvXTjaXFw5aEr2jtiR4Fx1MDAxOFF5us7MkixLVPVcdTAwMTUv4vepUKUzvrefXHUwMDA2QddcdTAwMTmmXHUwMDBleieEWujWOkOph854ZFx1MDAwYqRhR2H0KIXDXHUwMDFm1ZlcdTAwMGLzXHSJ7uKhVv9cdTAwMTG5/1FYjkbO7MCXJFx1MDAxNPxcdTAwMTRF+Fx1MDAxZqtjxSlF2slcdTAwMGZ+TFFcdTAwMDbhXHUwMDE1js90tylrLENcdTAwMTbt2MusulxyuKKww1x1MDAxZVx1MDAxMVH89orizF9cdTAwMWVQXHUwMDE0KlxiR+JoW3OAteBcdTAwMDHC1lx1MDAxZv/948meL1x1MDAwMqJcdTAwMDBcZms/XHUwMDA1YIIxycRcdTAwMTmDN5adXGLelVx1MDAwYlxu5DXcoo00ndJp0MNYLbgljXhcdTAwMGIpNNfuXHUwMDEySy6IJakpLFx1MDAwNlx1MDAxNWDObNs182mndfVjIVx1MDAwNJeCXHUwMDEzbzNxtXzZ52ZcdTAwMGWOm/ZcdTAwMTdL9JGBMv7pXHUwMDE5zEwm4Fx1MDAxYz7+nZzyyXQ+iY+fXG7l+4fQ83rgmOBcdTAwMTZIud4rXHUwMDE2XHUwMDE2XHUwMDE3llx1MDAxMzbsJmvW9oVXYFx1MDAwZuwmxGPcXHUwMDA2JYYvzJEs9uFIvmPzh2m51bJcdTAwMTP5ZK1XLFx1MDAxMjVcdTAwMWNe3fFXrUihue5VXHUwMDE5sofjNI2wx3pu6fSlsi/rtEw9LGP1yabQjqfvNqtW2rmgnptuX3TuKdQuXHUwMDE0hsnXhl3FO9e4jaOzMNaZRucus9pFO3amm1x1MDAxYm04joi2c8Gg+Vx1MDAxMp29jvlsXHUwMDFkb9yFcSVXL1ZdXHUwMDAzm+rV0VO0ukZzmq3lIZq17/lEOFx1MDAxN8yyPCFjjWm31F4slpPNSyFcdTAwMTR2jdto07hcdTAwMWE/pHE1Jsp5XHUwMDE2a47v7vqutLf3sFxmx4Lm8/28XFzOMDJR9+PxKv7S7eS615lcdOXvm1x1MDAxMdNRIdhcdTAwMTms57nJvJQtNcqbZatRXHUwMDE1o2xrNeuGgq3Dcm9cdTAwMDY8djM3f9zK12q00m/EauR/XHUwMDE0+u9cdTAwMDVfoNCr5mD8iGKbziZXyZPUXUxcdTAwMTCqXFxcdTAwMWaxK65ytdmm0l3k7V6yWirUO+JcdTAwMWHqiJTvWHShIVJiZ1x1MDAxNO33yv1R4m7aKJdVup5YKZaObkZcdTAwMDFXRrlH+CSJti5rXHUwMDFivNioXHUwMDEyzIX71v+PXHUwMDBlXqqDncdcdTAwMTZfle9iqzZcdTAwMTFRjOujzX3WNWAquZhVUlx1MDAxYcxcdTAwMGbO1pPlZVtcdTAwMTaneO5cXKBxNteYL0Y7vCmz7mMvvW6EQ2dp8a6SKU9cdTAwMWaf66NcdTAwMTeVwXjxsFDFpGtcblZT0H7Onlx1MDAwZVGkXHUwMDFhX9w/Z5GIZ+uXJVxytaOi9OPhkDRcdTAwMDNcdTAwMTPoXHUwMDE501x1MDAxMkSGPPbyLUmfwttenKV5b/XQXHS2hlx1MDAxYnLQXHUwMDBmPjO7jNfVT70x4vvq7Vx1MDAxNfExU8h6tHbulsp96dSqWqtfc96kP5jqwNK/71xm5FtcIinO8CeT+O6pv+Tx10Gxml1kK9txpuPXd1x1MDAxNlx1MDAxMFx1MDAwMZfIb9LAz2GNUlxct/3skonWZi5cYuaGT9xT+P+sVMe7XHUwMDAzu27XgpLDc/I2fSxFYVx1MDAxMp01elx1MDAxMFdSM4W342ZlMZznXG5cdTAwMTmMKrlVsMVcdTAwMWHQ1D1RI8SQ/FxuSdZcdTAwMWXzXHUwMDAxPCSZXHUwMDExrVx1MDAxMPdj6P6zkjxcdTAwMTi3Q5PuYFx1MDAxY1x1MDAxNFFm3LfCn1x1MDAxMWXKXHUwMDFlz4DoxYxHmlE7nbRpe/NYy4jXQkhcdTAwMDRblqnhv5SYSzO2gIPUuFx1MDAwNVtLQ47JpZZIYMM9cVx1MDAxM8Em+iT3hGDTdyqw+ERcdTAwMDHn7YeRXHJAJn/894+CPVx1MDAxOc++WrpcdTAwMGZcdTAwMDfSgvlcdTAwMWW5mDCaXHUwMDAznJ/uZydcdTAwMTb5QfNRl8vJZX0xrJNyp1x1MDAxNZ1cdTAwMDdbxjFcYrD1s0KVSc2p+iDkhlLFUMRcbiY008zp/35GyrVotmyPQ0fOqOUh55i5gvpfoo6ENOMujtZcdTAwMGa+ju1Qf9NYqG2lNJIqXHUwMDFhX2ZcbtdvY7lCnD2dpl9ao+om9op2u1x1MDAxZGt1bCmvXHUwMDFi42ayvSmZXHUwMDBmUGvajsRcItVcdTAwMDUv93PpaySihPItYjQ8s4qLc/ph0oWQimVS5S1+rLVmI01e2yRcdTAwMWF0XHUwMDA1YsxS6rdcdTAwMDJ9nLqOkVx1MDAwNFx1MDAxOSZBUiBDoqZcdTAwMTg62lx1MDAxYv9cdTAwMWajP4t4iG5ecZpNoupp2Fx1MDAxZWWGoy69hvQ7ulr3yyEx5Vx1MDAxMMeecWKfj6FpOXzHYpmIrSuLlOxOVyTo0k/0QenHXHUwMDEw5qJAST/YXHJcIlx1MDAxMVfoWIXXt1x1MDAxMf9jaVrRaYafXHUwMDFlSynV7C71XHUwMDEwRDQ+fVx1MDAxZThcdTAwMTf8iVx1MDAwNCcmyjfDqbmG6IKfUeyFU2U5XHUwMDBll1x1MDAxYTKdKbSGW90tbFZ+TK9BUVx1MDAxZEXQYc+LM0ve3PM6MVx1MDAwNaSoXHUwMDAytNM+9S5fozRcdTAwMTdGIe1xaL6ozTvO+/Sn41x1MDAwZlwi/NP8WEhwXHUwMDFl2OlK0KpvUFwin6WTx8XqZbpNTHHh4ctKgz/H4GF4jrVmpvBLSs3kx1x1MDAwMVx1MDAxNJpZb1x1MDAxN4Q01KHowk5ihVqtOtrXXHUwMDAxSs1cdTAwMDBLiPMpKCPiXHUwMDE0e2hcdTAwMDRcdTAwMTKgrEwrpMHcaWfv8d92XHUwMDA1XHUwMDE5YlMkj4Ylvdet6HcyoUSv0Gbx0bzYSoavb1eOXHUwMDFlveUqXCKajlZcdTAwMTO8Wt3R9Gs4XHUwMDFl7SxdZiPbTnRCq8063F7Nh/XuM3sovYau4DU5XHUwMDA3x+85TUqYgTnq9Jjbjj68RO5X41K321x1MDAxONS6xVxitef9YFx1MDAwYj3X2GJcdTAwMDI8IyFMje9Hcm+CsVx1MDAwNXdAS0rVXHUwMDFih2qAZZ5cdTAwMTmeXHUwMDE0LXzahr9a5sfIfrgvb+eNymM/223k0rJed4l0gJXCf7QwXGKBkNI5qfqYTmRmtVbx2ZDJTcuPs0kr3Y/dfVnp7yd1QoKz4zhcdTAwMGaTXHUwMDFmdIJcXOb93Fx1MDAxOPlcdFx1MDAwN1x1MDAwM3Z8xtd/uFx1MDAxNjhcdTAwMTYsY+F5Jlx1MDAxZVs/k20/m9w9LCv5qYsnqlx1MDAxYs+uQ9GRXHUwMDFki+lFM4ZcdTAwMWZcdTAwMWYqXHUwMDE56nqHrW6NO0lUbVxyM7Ft6LlcdTAwMTZcco3i3Fx1MDAxNbS8zpakLFx1MDAxZeqLRJtNXmov2dGzqzLkaOmId7nlRVGNM1v/MVx1MDAxZMZcdTAwMTElSHN8ejos9dpCzXy99Vx1MDAxMFx1MDAxZqxG8VCzkIzObFx1MDAxZj3u1Fx1MDAxYZ2l8+pcdTAwMWZz6fTPQ5NfXlx1MDAxYv6gyYJZhGJNXHI5ISjVhbPC/fTaWa76rsn7JVucXG4zXHUwMDEz64/aL8+whpkhXHJcdTAwMTD6USVcdTAwMTUlwpFXOVx1MDAxMuVk293RxnnT3kNcdTAwMWPtWvxcdTAwMTVccl3+XFxJZt4uePfntLjby2yN3j2J0N1cdTAwMDRUetyuJ7L53ddpXHUwMDAz+pQ2YIbAm0OGUUPA98XaXHUwMDFk4Vx1MDAxOHZOS1xiM35cdTAwMGYxyehl8Y1fiVx1MDAxM0cnMSVxbGj0j1x1MDAwZjfITEbLUqEw5LmdXG6FXHUwMDFlJ4Pua6ZzdTOm0+twZruQk+wjXHUwMDFk2PNMuLOmkf1P+VQq4G7ZXHUwMDFkNH/8949cdTAwMWPcxFrbhj9F7clg/NXMSlx1MDAwNz1C51SfjyeTXHUwMDEwKVMnpeJR3XlMbIRKhLu9PJtlY4PIcyVcdTAwMTf1Sy3fQHc+mSGjXHUwMDFjOXTHzTHGhXYpzkXl94tZbTSf1GbwRPe1R2CP0lx1MDAxMm4qkL2r//+uXHUwMDA2psIkmeWxivxcdTAwMTKWyX5q9/Isnpb1XGZJNUqrkstN+Vx1MDAxYXU72lt/lGyvX3lcIk/VReEx2V/1XHUwMDFi49xsvWy5Jrw1Uq1cdTAwMDdVW1cq3Vx1MDAxOX+qtlx1MDAxZWqTNXa5dEe78y/g8zuoa5r5ltMzQbRW9Fxmr20ra9O7Sa08XHUwMDE4PT1lXHUwMDEz9ex6Wsg9XHUwMDA1305hbkmHnXJcdTAwMWbkXGImXdp2m1JcXKakz1xmXHLHuIt9PiGmXHUwMDA0pfz4vPPWdtt5SKXtnUyNpoPY+LX/irdO8XpYr0RpPcuPq4tMa1todZLUXHUwMDFkUtxQV493hb5/ylGiXGZU6HbtyfRpXHUwMDE4m710MpNWMVZmLkVq0lQ1XHUwMDFhX9eXkf7DOj7qXHUwMDBlZpllybnAm3vzJE3z9Vx1MDAwNv1n+3GiISRwXHUwMDA20EdcdTAwMGZ8yHY5bWZlY9QqxaPRZHcmJiz4SkaJS8nczqDEysK3VzKH43eoUlJTZPg76bfWqptcdTAwMWYq3Y3XP0K/vMdcdTAwMWa57uSn03hVN/KUUVx1MDAxNUe6xfxcdTAwMGI5qVx1MDAxNkJrfFx1MDAwNlx1MDAwMU1qXHUwMDFk0anGsq3vspPXSqjSnCVXfr2bXHUwMDAx0jzQrXfNI8jdkK00cWneZY1ktpkmw65l3lx1MDAxNGzaTEQ85keyxDSXlqyUmTw12lx1MDAxOcV300ghXHUwMDE3XHUwMDE0PfzPt25S+ibwzZw8iTU+I16jJFJb4XSsn+DZRoKkXHUwMDFiw0ioXHUwMDE4eCVcdTAwMDNzccCHNGy5Tlx1MDAxZvKy6TB+SobZSTxPhlx1MDAxNVQx5qCz+4ZadfuxS/ao31x1MDAxZM2va88uS4so4lsxgbnSXHUwMDAw9PRcZlx1MDAwNsJO5HX10K/XS4P29I6GerFFWGV99CxQXHUwMDA07Fx1MDAxOEtcdTAwMTf7wMczZPFhXHUwMDEwykW61mpKXHUwMDFie+hcdTAwMWFRzKvsjsl9fmksuFx1MDAxMmbA6zFvMr6UKltar+usb6NE+TE+T95d3cRcdTAwMWM9njp6htZb5Fx1MDAwYjyaiNpccr2u6kF1suN51zssorrTz6pcdTAwMTVaZePPYjhY1O9cdTAwMTIuso+jJ2DeXHUwMDA3XFxcdTAwMTdcdTAwMTkpof2Vh1x1MDAxMs5cdD2HKHlWuE9vXHUwMDFhqedYtbqtkrhcdTAwMWS6izzXfZQnMEV32JW2o1x1MDAxZk6nNHf1ql3K2e6jN541d5joPa3hQlKClbid1tzchFx1MDAxNO3Zqtuw/z16XHUwMDE415r/XHUwMDFl3dVcdTAwMDY1XHUwMDEwOG/KQUyw6/e/ZIhcdTAwMDHyTbMrKlx1MDAxMfxzhjk5rNNcdTAwMDE2J1x1MDAxMFx1MDAxMbn04kPvvdKWXHUwMDE50SQ4+G/gYF3Yie/rulx1MDAxMeaOjzyo2KiFNFh5XCJcdTAwMTWnXHUwMDEyQ/S6P6NbXHUwMDE4enFBfLitvnpi1mFcdTAwMGbjh3uUXHUwMDAx54JcbsnMmbSm747sj98zXHUwMDAztMvoo89OMjhcXCDk2Fx1MDAxNLIoJ1x1MDAwNDGNKEOUS+acV/X3eFx1MDAwNVxyXHUwMDFid5yEkP1NXXeQga+YmFdoX0Le33BcdTAwMGZcdTAwMTFcdTAwMDM108+fJlhcdTAwMGKGXHUwMDE1PcexPVxcXHUwMDFkXHUwMDEzYCTiXFy5ZNyNRFhYXGJjXHUwMDA0XHUwMDEwpFx1MDAxNGJcdTAwMDKxy1xmtC9cdTAwMTJRjyM/8HUhhMdmhDxmVJL9Iz9NmFx1MDAxMTifoPKrh6icrORcdTAwMDZ5iJCmz9mwhVx1MDAxYojf13K48/KIlp9cdTAwMDQ9J+OhXHUwMDE5XHUwMDE3SjhcdTAwMDRcdTAwMGVaMlxi1LljXFzCO/RcYpfV8sLD647rc1xixtvf92RcIlhY499843/gaciFJCfnVOb0hdyk4/F4cXH/Sif5p9g0071cdTAwMGJ2IECYXHUwMDEyXHUwMDE2hDtCMcRcdTAwMTUj/IPHozG1XHUwMDE0vFxyIFxyXHUwMDAxOaQ3Yq2gXuHz/lx0p+kjxFx1MDAxOFx1MDAxZj3hbKyjs2WLjuL3hcJzqN6Yvuafb9t9QyzNwC1kplx1MDAwN1CD33JqWJBcdTAwMTmPuouxXHUwMDBm8zhxLf86Llx1MDAwMHVgoqVcdTAwMDBcdTAwMTExXHUwMDA1+ScrxWjHaM9cdTAwMGVcdPtx9Jhd4mQkXHUwMDFiKi2Crlx1MDAxNFx1MDAxY1tcZlx1MDAwMM3YWCHlXlx1MDAxOEDBNjOGYVx1MDAxOfjYlF+UVTpYc1x1MDAwM5+PLVNcdTAwMWOniZKcYq+criRGgZE5wFwiRjn267Jcck+Ado0h9VZcdTAwMWKSyzbjbV15XXTqVVx1MDAxYm5w5pW7XHUwMDBlXHUwMDE0bqlX75/SXFw9L5uDbCa53ejosJZ5SiW5a45tptNnu2e+jFRQvvXSnz82kt2Oc8HRZtL1YDRcdTAwMWVcdTAwMTbnlfhmw+bTx1Fr2Vx1MDAwYrumXHUwMDE2XHUwMDFj7TZd5lx1MDAwYo1BMlxcQalcXCtcdTAwMWLXq1JrSV1lQJ1toTHaPXTwulx1MDAxYnkp3Cm22lWizlx1MDAwNXE9TubLNJlcdTAwMWPmo+Hti93OxFtPzlx1MDAwNfeD/K66qvBcXLGYavLZ9CX9unD1q1x1MDAxZW34XHUwMDFlt59cdTAwMGKh5F33rlAsb7eheHi56O9cXFx1MDAxZlx1MDAxMU2sKonHKJl05vFQuL0o2tJ2LignK+J5XHUwMDFku29Ml6XG7HHHR7LnSt+lkvXVplx1MDAxMF/oWVHvcGZcdTAwMTVZr1x1MDAxN66eWW/KXHUwMDA1x4LR6+SlzPOJUj68jUR7vN1kYddcdTAwMWVUJ37XaMrUbsnDKsJxvT16djf2htQkt1x0r1x1MDAxM7nlMDV5Wa1b5ZjrRqUnjV1rkWtHRy+xwjxRXHUwMDFhRzI7XHUwMDE3X3AmgzftzD0jdracLW0r69Q65fpcdTAwMTberfOOXHUwMDA1+XDI3inVbc+n26qdXHUwMDBmrXstfeJR2uHOSHRgPFx1MDAxNiVcdTAwMTKfNVxccqlskt7p0oRPcDc9mqYq4UHZXHUwMDA3jFx1MDAwM1x1MDAxNVx1MDAwZVFwai0suMlpaHBcdTAwMDA+NEhKrcEtx4hiXHUwMDA03orSXHUwMDE3VUH+l92qgbPjkefH2lx1MDAxMuDeXG5TXHUwMDBmQpGUXHUwMDFlcEwlM43+jMOjIVxia+XRLIZcdTAwMTQ34fox3lx02lpcdTAwMGZsnEvo59Zo3blcdTAwMWJcdTAwMTJcdTAwMTmPuFxuXHRcdTAwMWbkbFx1MDAxZEfpx/RLs17rqVx1MDAxOVx1MDAwZWVrU+eCXHUwMDFiXHUwMDAy+lx0gdrnXFxyTXxcdTAwMTORmEpFwVc8o1xiMZNYt5I5sqnW5fZJzVx1MDAxMs/NRsnvYCsg3lx1MDAwN0XgfVBuMt5cdTAwMTDsUTD7bllcdTAwMTfgbMJlpeA6o+oy7+OAS04sppmZZSZcdTAwMTDm1CtcdTAwMDepzFxuzVx1MDAwMYBcdTAwMThcdTAwMTVKKVx1MDAwZo5cdFx1MDAwZW9hnJf/cFH3iVx0XGJRlJpcdTAwMTFcdTAwMTnaNK8w16pDc4lmtVZt5ENRinxv+pdcdTAwMDZcdJr6R85UIUH1OZQvtJ2UjbKtN8/jx8mzzE9cdTAwMWKZuFx1MDAxZnFpYOryXHQ3hYpcYp6FQlRQ+aGYKsQ1hNbStEoqqbC88LSg2WI28axjXHUwMDE0llx1MDAwMJNnXHUwMDEyVmAktUfrJlbIQlx1MDAxNMyVgiCBKan3XHUwMDBmXHUwMDBiXHUwMDEwQ0RDmH+M11wiVXyURCUzm9qMvC70loVUKfqNXHUwMDE09XM2SXF/m6SFXCJIO5k0jzahqGSHdJ9Ku6U90aFcXGazeURf1qr/SVE37F6MXHUwMDAzlCmQMsPC8kHUibQwMzlfo1x1MDAxNM6zxOuSm4JvZY6BzVx1MDAwMGtzTO9hk7Q5qVEm/1xm31xuVG+/XHUwMDA0g1DQXHUwMDA12Omx9q7vLuqeNolaXHUwMDAwXHUwMDA2xLCWXG5ixvudapJys/HQXnTspU9cdTAwMTFcdTAwMTT2ve1fSiODiG/qSlClkXR2R1x1MDAxY1x1MDAxZFxiMVx0P99cdTAwMGZWu/5cIlssrOTDk76XhW9glEBTmVJMXHTEXHUwMDA021x1MDAxYtdiSDc4p2ZwXHUwMDFluGf4supDppWsi89cdTAwMTkloS3EuUDmoIVcdTAwMTLikbZcdTAwMDJ/n55ANc8q2cYqa2dblFx1MDAxNW2ZLPZcdTAwMTSXkW+kqJ+Mk4SvTZKYI0yc826PiXotXHUwMDE2ra53qM3Zbt3IbaOdMGpcdTAwMDecOJ6AeFx1MDAwM5gpsExcdTAwMWFcdTAwMDJz/YE2zMxBgUtcblNGIIo6LOefM0eEXHUwMDEz61cmXHUwMDAw/HBEPYqZjpsjTVx1MDAxMfjL+Ohs4u8u5Fe1RjFzhthtXHUwMDE07dqs4cNjXHUwMDE2XHUwMDE0g+Q/KJxcImHCwtOTd73+ZrhI69YwgYp4XHUwMDE1i09cdTAwMWXuVzNcdTAwMWYlXHJW8o5IbDRFg2vIOf5IhVx1MDAwMapjKYKJZIbe59KCeNqsq1rdQ1uvk7zjXHUwMDA0bJV2XHUwMDE2KXrr61x1MDAxNJeLsteM9kv95Pyhn1xiPbLC/Fx1MDAxYunrJ42S8jdKcFPpObnqejxZ7ZR7/fv5xo5un/OZalstg22TKEi6xSlcIoxcYsJcdTAwMDXby9xcdFx1MDAwYqxcdTAwMTGELoojflx1MDAxOceFn5hj7NHWSPdLasHtXHUwMDEyXHUwMDA0qWN9XHUwMDFm312MPc3Op1x1MDAxM3Ppbt03L3ezkt3DeThOfVxyXGbgnFJcXJxRwLJLriesutncVfFzrr/Rd3k74mdhglx1MDAxM/EwxC3CpKBmXHUwMDFlOHEyhf4kz+Tceitxh1xikDJxoXlptWogN/t6x4VXhpzvXHUwMDE3x0mGmUnFXHUwMDFky7SNev1cdTAwMDHKlVx1MDAxONv2lnYuVtWp0p1rqNzDvFtcco9cdTAwMWH1cTqDXCLb0d24gFPNq7eIVFKjJFqISiSaLEhaLVSfUsuxc1x1MDAxYptaOkZFdVAkT/Mpbydir/22a4F+ude9cqM7f64+dFx1MDAxZuhcYndjz1XXXHUwMDE34ZNSP7WOkJdxZlpLLedT2og7XHUwMDE3XGZWeFRtbjOJelxyp4rd/mxiP7taq2dcdTAwMGZPhfmg/FBNXHUwMDBlWuP0czpTnqVcdTAwMWHOXHUwMDA1MlTJqFZcIvlcdTAwMTKbi0dt73Bn2nh2LiiV+d1406O5VruopIg1xk3qarZUufh0zLbp9aRYKLw0XHUwMDFiqlx1MDAxOVx1MDAxNmFcdTAwMTeMRfVmTIbRVLPxOiqgp17uruVcdTAwMWHh1yigbrq0Wd/X86+ZXGatjsp2eeZcXEBX62JlQfg4nM+VJo3mJFx1MDAxMWq4idq6tIfSXHTFlrF8l1x1MDAxM9wrNsmdc4FYl1P56utrSI6zgy5r3XXlzsUsovLF+Vx1MDAxM25cdTAwMTVwV4Zmk6TaJbJcdTAwMWR0IYE1xPp+XGLEXGYhXHUwMDA1PqfhszR/rPKcPdaZMMmR3q6weM77TT5cYlx1MDAwZVx1MDAwMlx08C3BpIBnSVxi6PxHXHUwMDA0YtSiSnOJXHUwMDE5XHUwMDE22iRfboJAnlx1MDAxZJ9cdTAwMWXdNFx1MDAxMLZcbjD+R3nfb1xiQDfvtinYze7836PMtph/gP+MR+1x9M7Tbt+w18Z3npQ+cHTGwJxRfYbGsN0yVu6p+2Tpsb5tPtebqE38KK1cdTAwMDJcdTAwMTVcdTAwMTXCXHUwMDE3RVx1MDAwN+w2wYZcdTAwMTJbXHUwMDBiQlx1MDAwNVx1MDAxNpyKy7hcYnzd5dPmpSlcdTAwMDXPXHUwMDA01PeYu3xLm3yhSkRri5qR+/O6mFx1MDAwN3ZrcVx1MDAwYlx1MDAxZNDSl1x1MDAwNEdAXHUwMDE0rsg5iZH5o3597WTz0d3yblx1MDAxOduIh8ayXHUwMDEw8Fx1MDAxMlNTymQpM4TP8KVLIdxcdTAwMWNcdTAwMDEhqblcdTAwMDXWxJSBmEZudVlBU6tlXHUwMDBi5UFcdTAwMGZ64lA18FkhZMXOql9v8Y8+1KSK4CUqXHUwMDE306XXaLGCtuFVMCyCXHUwMDEx/1x1MDAxZuXazO6Ml/MrM9NcXFx1MDAxOMI5WfM+VvhxZJhcIs6YL/j0XHUwMDEyzlRf7lx1MDAxM6VBqJMhrH6X0U9cdTAwMGZB11x1MDAwNTPeXHUwMDAw1ICZLiRnQ9HPrIlUXHUwMDE2u7kqcOThPVx1MDAxMY/4TZnCK+m44DP/+KG+yz3kQqrCOJuFXHUwMDE31V10mXR6T7fUXHUwMDE1h0FcbkBs9cmoxLHgk/W7f9/MS3TT/0CZcUE5SOXp6ZXY60Dxbunp+bWaWKdcdTAwMDdcdTAwMGZ3mW1VXHUwMDA2XTflId2UXGJ/gZniyCO34qGb8DhcYmyGXHUwMDFko4j63rp5XHKpdlQr7dUzIXiOmp9Rz9Tutrf3m3E1vqmsXCKz9i6z068o6GLNxCGxJvRLxNoj9vAyOZxJLJFPI/9/ilg7TU6WJ2SsMe2W2ovFcrJ5KYTCrqaTo30tzef7ebmcYWSi7sfjVfyl28l1XXvYXHUwMDE1V7nabFPpLvJ2L1ktXHUwMDE16lx1MDAxZHFhXCJcZlx1MDAxMV+tMoMxND6j9uieR+1cXL5cXO32oovmmKfvQ6NoLOg6JdAhN1x1MDAwZesv0CnmlYb3XHUwMDE4XHUwMDEzjVxmOZs+noX/ozp1YchcdTAwMTNcdTAwMTl0XHUwMDFi/bdw58db9FPczlx1MDAxN/bwi0NcdTAwMWZ/wlx1MDAxOeVP7I6Ng33OyLdlo5osp0Vep9I00mhcdTAwMTaK2djj0EddXHUwMDAylVx1MDAwNKOmppUhQyqjiWnncDc2MSosxN84wzBlyOFrXjNcdMbMlFxyoSiShlwiVUmPpFx1MDAwMKHYQlRrXHSbUFx1MDAxY/7jQfSuzVRcdTAwMTfsqVBfzfkwS9R2etfPxu93OjxcdTAwMWY2Xiivufo7XHUwMDFknFx1MDAwZlx1MDAxNO4v55hcdTAwMGJC4CEwuk+vgJWliFx1MDAxMlJcdTAwMWKqXHJcdTAwMDXw9je9wZmcXHUwMDBmzzFcdTAwMTZcdTAwMWKwZ1x1MDAxZX/todpo+zTIt1wi3GtXyFwiXHUwMDAwpsRcdTAwMWPRa2Wm5+K9PVx1MDAxMfPIJCVUUdg+6Fxu3dvTdSlcdTAwMWZ8hcS89sXj/f32sDBIZDOOs5g9tlLMkXZF+kdcdTAwMWRhz0FK31x1MDAwMIVcdTAwMTTHLrKZdyX+yaJ44cxV3+Q78cq5eMyQkFxcSVx1MDAwNlx1MDAxYVx1MDAxMVxmcDmHUFx1MDAwNmPKXHUwMDE5w1x1MDAxMvDDdOyJfUJcdTAwMTnyt+qdXHQmOznlk+l8XHUwMDEyXHUwMDFmP1x1MDAxNcr3XHUwMDBmoef1ILzxXHUwMDA2XHUwMDEz05khMcdaYS6xg53ifVx1MDAxM/i26OF40uZcdTAwMTWCh/xdXHUwMDAwglx1MDAxZFxilYmhKTsnXHUwMDAzdPiIP8BcYmHGz7jYxtyUXHUwMDE4jIOQSVwisaJagSNwXHUwMDFivKCIuVx1MDAwZVxmXHUwMDFkztL7gbdcdTAwMDVQXHUwMDAx9omB0YQnQ/fARGipqVx1MDAxMCxcdTAwMTjkVGjbwcWFvS7VXG7V50biuUVj7uqZdy1GZjpcdTAwMDSXyFBUMXOzhYdcdTAwMWG7YVx1MDAxY7NPYsvBOrBcdTAwMGZcYschssVcdTAwMDT+p1x1MDAwMOyY8lx1MDAwMJdcdTAwMGZcdTAwMWO8StxcdTAwMTZs/MXEvPZcdTAwMDTku1x1MDAwMFx1MDAxMfU9McUgzVx1MDAxNOTiXGbO2sOlREFcdTAwMDZcIqpcXE/XjUNcbsIlyiTWklCOblVKTt2FXHUwMDBi3KPiVlhcXGkqMTFz5CRlbK833vBcdTAwMThCyMeD4dOco/GwbVxy31x0XHUwMDE5inHCPUOTY/p+XHUwMDEyXG6dgY1cdTAwMTjiJVx00ZvCyDFs8X1H/CguXjdY8pWQt6v7wvFNMFxiU+k/11Uwjd683ZNB6PDBYJBBiJkmS67BrUZKaUw/0NFgISz4oelcdTAwMDBcdTAwMTDwlIm8LNnp29KCqEVcYlx1MDAwMyCRpvXai0SPMGFphaiJ2E35Id0v7sBIgmtB6Cd4gv8wXHUwMDEwXHUwMDExqeCLSSRcdMQ0oP37io+xeVx1MDAwZSbVKKVhYqCfdIhcdTAwMGVXVXyER1x1MDAwZVx1MDAwZVx1MDAxYVJcdTAwMWFcdTAwMTRCc4+IXHUwMDBmw1NTJipEXG62rpFHOum6aOQrJz+v7onIN0EjrX3P5k1cIlJgoU6vITtcXINcdTAwMTBkLIIvainMIDLA4OtcdTAwMDJcdTAwMDC7scjMbXeeu+Db5JBcdTAwMDFcdTAwMTKtt0NcdTAwMTWOmYJgwPPIXiqLg1x1MDAxMFxuXHUwMDEwfMZccsH3RzBcIlxiXHUwMDAySIOnwYjOzlF7YjiDXHUwMDE5XHUwMDAzXHUwMDE0XHUwMDAyXHJcdTAwMTLcafLfWcv/NvQ3jMcoxGOAK8w8a5M0299cdTAwMDexXGZdpJZKXHUwMDE4alx1MDAxYYTF/q6uij/+ovF2dU8ovlx0/PhcdTAwMGb1JtjE55ycXHUwMDAxP9VSY52k/VXteVNcdTAwMWXWVT7zRKKr71x1MDAwMD9cdTAwMTh8WDfgOFx1MDAxMeiaXGLj1bC7T3YqXHUwMDE011x1MDAxY1wi+0Dgx8lcdTAwMTFcZugtoDdcdTAwMDFXXHUwMDA27K8xxJoqXHUwMDBmmm96pfTO6aljiKZcdTAwMDFIpFwiXHUwMDAywE07eJB+fFXqOCTdqWNcdTAwMWQwfDhYaXUoe0xcdMVEXG52eqHV4fPDXHUwMDAwg4ThJ7ZMmMFMwy2YnVx1MDAwZqfcmlvUTKsw/1x1MDAxMEkv649iqIG43MdcdTAwMGbqNUeFOVPVv1x1MDAxMURgTNhNXHUwMDEyM+/ac7hkXHUwMDAxXHUwMDE2RDrtUDr7Usun7U0hOVx1MDAxZSR3VeTqOLx80Ph6XHUwMDE4r4cqyZjs7TKJUbipn+qt3WVVUNRf4k0lu2HJO/1AVS2jg1L6ZbGNyGZyVVx1MDAxObw+7oZ+042DJfDg6FhmWlxmUZSBR8jclFx1MDAxN0Ixw3ihXHUwMDAxXHUwMDAyOKArulFvk8NDOdTdgVx1MDAwNIL4XHUwMDAwqVx1MDAxYmdcIj9Vw5ToLlx1MDAxZZzf7atcdTAwMDZcdPunwLD/gFNsiOXA9zuj4/6wilx1MDAwNlnCKVx1MDAxMVx1MDAwNyBdUm79pKc2RLZSXcZ92WrqmlBcdTAwMWWQzkCPhOHaoIJcdTAwMTl6XHUwMDExj1xmXHUwMDE4hvCCXHRcdTAwMDLPhVDNsdiXf1x1MDAwZfppSrCC4TOeXFwk9FZdwKXUXHUwMDEw2Fx1MDAwYrB4VO2n4j/rIT4mNkIlwt1ens2ysUHkuZKLXHUwMDEyn01cdTAwMTiIXHUwMDAzk0m1UEJ4ea3u41x1MDAwMM5uXHUwMDFjbob8pcK89uQhWO7kgVx1MDAwMX3SfyxcdTAwMDfIr6E+l6eb1p4q2nL80qzGdqtB5ul1to32XHUwMDEz31x1MDAwMngg4LS4XHUwMDA06ylcdTAwMTXYLK7dptWcQVvCpOY1oaBcdTAwMTJcdTAwMTeyffhcdTAwMDFcdTAwMGYm1ptwSayUpICAXHUwMDFlwEOkhVx1MDAxODf8Mlx1MDAwNl8w32PdNeSH5uQ2XHUwMDE4wJOqhKv3k1glw0dZMlx1MDAxOLZCqdZ9xUfnuVx1MDAwNJVHRGv4XtgxXHUwMDFl9MelVU0nXHUwMDAzXHUwMDBmxFx1MDAxM1xmXHUwMDExXCJcdTAwMDRRXG7UWFGuvWqrvlx1MDAxYXp85eLt8r5IfFx1MDAxN/BxMv5+XHUwMDAwXHUwMDFmbjJcdTAwMTGUnFx1MDAxMceGdu1SUm6eZqH7cmdlz8f1Sed7JLso0ZaCp8pcdTAwMTgzJMjSjT1KaFx1MDAwYq5cYoFcdTAwMTk3x/tcdTAwMTfyfPhgXHUwMDBm09hcdTAwMDLsY1xiXHUwMDE5ujGsvHxcdTAwMWWkLcPFxk1CSWnmsFx1MDAxY38n2lx1MDAwNTJeW0Cg51x1MDAxY3dcdTAwMDMzQHZwsVx1MDAwMX5cdTAwMTSWmnrkt5HJJoBXh8FcdTAwMDOQhH6y/CDxmFx1MDAxY45cdTAwMWaGo24+VOk1KrvGqL/R3lBkXGI7NOdcXHKJOXfVRr/n/s24KFx1MDAwNT5cdTAwMDcjXGJpdutcblx1MDAwNEBEXHUwMDBiXHUwMDAybmKIXHUwMDBlhSbUXHUwMDA1RMyMK1x1MDAwM8GQhlx1MDAwZlFxxY6+XHUwMDFkXGIziDagvuFm0Ew7341cdTAwMTDwXHUwMDAxkVRcdTAwMDRcdKreXHUwMDE4cI6+na9cZr9dXHUwMDE1wlwiILpcXFx1MDAwYlxuN1x1MDAwYoD0m6Ck83R9z0OTIFx1MDAxNlqS01Hy8FSjXHUwMDAwoyTB9MORXHUwMDAwNr1fnClcdTAwMTBcdTAwMGatQEfkbbwyw6khIORcdTAwMDT5JFx1MDAxMFx1MDAxNXGvelxiYlx1MDAxOHk5gVBcdTAwMWScXHUwMDA3+O8+P7w01ptcdFx1MDAxYYzCrHQ0VVx1MDAxZdYy4dhDt1x1MDAxYu9cclR02mBcdTAwMGJcdTAwMWZoRG+NoiBlmIi3yqt9XHUwMDE4kq5cdTAwMTNcdTAwMDTGP3kgeXion3tXXGa9RahcXCnQaOlxgoCFx7noVdHQVzDMK7QvXHUwMDEz31x1MDAwNG+I9HXKiGFcdTAwMTUh4pzppd4tsd54XHUwMDEzXHUwMDE46jXMKFx1MDAwMcsqwFx1MDAxM8DgXHUwMDE3OJNzv2awUIuCXHUwMDFiQI3tMmO3Llx1MDAwMZ6Do1x1MDAxYU2RXHJcdTAwMTLKXHUwMDEwXHUwMDA3XHUwMDEzYmqiPaZTIFx1MDAwYrBRMm3Gm5v4lbO901x1MDAwN/p2XGaIfKZTfDX65CbzUrbUKG+WrUZVjLKt1axcdTAwMWLyK0+XZta4cW1cYkeKeNSBXHUwMDBi95PC+y1rJ4HPYb6BXHUwMDBmkEhcdFx1MDAwNctLwE9cdTAwMTSm1GpcdTAwMWZ8sPXmu0ku3mbNy1x1MDAxYjtm/mJiXiFcdTAwMGZcdPkmYIRcdTAwMGbMyIGImLvn9lx1MDAxZZ3bdnAgY0DBiIDva5mRXHUwMDAzXHUwMDFhI8NcdTAwMDXJP4KRJNZbhVx1MDAxNuVcYmIvdFFS/FxiXHUwMDE4XHSIgTBnSGolMJZcdTAwMWUnQcQyx5wmc1x1MDAwZn6ZkpLvlahcdTAwMTPwvzV1+qzfpVx1MDAxOFx1MDAwYlx1MDAxYsJcdTAwMDWAfWRidSSkZ5CoTbuvaVx1MDAxMDFcdTAwMDOByCcz5ed4QlxiPCAlTf5AcMm4R76egJ9CtFx02CBCgtBcYqm9XV1cdTAwMTmN/OTEvEL7XCLyTcCISP++XkRcdTAwMTXEvvKMrr2moP2cPVx1MDAxZKJINb64f85cIlx1MDAxMc/Wg1x1MDAwZUZcdTAwMTgjXHUwMDA2loZgzZHp//xYpiU5sVxmvVx1MDAxY6ZcYuCZittccrHmmFgmXHUwMDExI5hhPeTKq+lXgFx1MDAxOVx1MDAwNndcdTAwMTVcdTAwMTRcdTAwMDMx5CxcdTAwMWX9OyqjXHUwMDE0XHUwMDAwUynvWUBfXHJFvXJ/lLibNsplla4nVoqlo5uRX/gjNCdcdTAwMWE8cVx1MDAxM/9cdTAwMTLtUUBcdTAwMDVcdTAwMWWq4zlIsa/zJyFRPoam5fBcdTAwMWSLZVwitq4sUrI7XfmkzsG1MKxcdTAwMDdG64VzLvhcdTAwMGZ3ibpcdTAwMTZcdTAwMTR8UXhcdTAwMWVcdTAwMWVbuipcZvlKiHmF9oTju4CQf8dcdTAwMWUzY064PGNC0+FcdTAwMDHUXHUwMDAxxSBihq4oSn9aYazEh8kvhjLJkpxcbqS4YVUkt/OIpIlcdTAwMTOFoIhziFx1MDAwNVx1MDAxNfNcdTAwMTjWZLp2QP1cZsOjlFx1MDAxNKP93mHTsYW0dFx1MDAwZSD+o7mhQkjFMqnyXHUwMDE2P9Zas5Emr21cdTAwMTL1gSFcdTAwMDG+KTLJVFxyTlx1MDAwZvNwiCg2Y+O4QJpcYsGEkJ+EoZNcdTAwMWRcItiTXHUwMDAw1Fx1MDAwMyeTI3DU4Mbvw5Bp51x1MDAwNFxmYlxcY27mXGLfODjzXHUwMDE1XHUwMDEy89pcdTAwMTePb4JDXHUwMDE0+XdcdTAwMGVD/ElccifE6Wlp70H3QVx1MDAwN1wiramlhTSdbmBi9YfAjDJ47kRcdTAwMTlcdTAwMWV/XHUwMDEzljmvXz0yg1x1MDAxOFBcdTAwMWGjakRcdTAwMWXie4/AXGZiXHUwMDA0i3FsmOBcdTAwMTWCpVx1MDAxZc5cdTAwMTBcdTAwMDa/XFxzXHUwMDFkXGZcdTAwMTg6Q+NcdTAwMDGAkDZTabTilHpQLYEzJFx1MDAxNJZKMIpcdFx1MDAwNNGfZFo6mf9cdLakXHUwMDA1klx1MDAxMmuBkVFujz1x8JMhnJemd1xuMa/zxOvGZH5cdTAwMTJiXnuy8U0wiDt49veyQ8owSpEzSK6965aDjkGcmkNgMMFIczC1bn5EbeZVXHUwMDEyMyxcdTAwMWOZ2VT8476uh0DgcYk3OjOIXHUwMDEwiaaIebGoUFx1MDAxMDOjgOBcdTAwMGWAXHUwMDA11sJjOiszc95lMCDonKJJZFx1MDAwMlxmwohcdTAwMTmSatq09jFIWKai0lx1MDAxY05rZEKlz2HQyfVUsCVccpqOKFhjXHUwMDBljlx1MDAxN/ZgMFBcdTAwMTZHXG6D1CAzYFx1MDAxYuFcdTAwMWJcdTAwMDdkIX8hMa998VxiXHUwMDE2XGb5Tl45kFx1MDAxNpJcYlxcZCfT2TFcdTAwMTDK9VuJXHUwMDA15lx1MDAwZjjaXHUwMDEwkWGnabPHZ7/z+cCAUIiZLlRuWDnMSFx1MDAxM8XdWSF4XHUwMDAzZlHJXHUwMDE5aIiZU3hcdFxi/ZctmPbiaNXSg/dcdTAwMTjLvWiLS82JYJ/J+XzBgKGWXHLw2rDn//r3qDbp/vjvXHUwMDFms0njXz86i8Vk/q9//rPdXXSWdasxXHUwMDFl/nNRXHUwMDFihFx1MDAxNnaj88/2OLSzZ+N/j3rj+tGV06W9tP89gj+Bdsw7R9e/rfLsk8BcdTAwMGVK3M/NdvnHL3X+qzaZXHUwMDE0XHUwMDE3tYX9XHUwMDFi9eCJdJu/bt670v616trrO/9Jyv8w7/n//j901fBEIn0= + + + + WebApplicationFirewallAppWebPCCDNDev TeamKubernetesAPIRPCETCDJobDocker RegistryGitLab CI RunnerLint & TestCodeKafkaFilebeatlog-pilotAlert & Reportgo-stashNginxBuild & Package & DeployBow - Deploy PipelineJenkinsServiceLoadBalancerMonitorGrafanaPrometheusElasticSearchKibanaRedisMySQLMongoDBDatabasesData WarehouseClickhouse Data SystemGitLabReferences:api & rpc: https://github.com/zeromicro/go-zerojob: https://github.com/tal-tech/go-queuego-stash: https://github.com/tal-tech/go-stash \ No newline at end of file diff --git a/go-zero.dev/en/resource/author.jpeg b/go-zero.dev/en/resource/author.jpeg new file mode 100644 index 00000000..c566910a Binary files /dev/null and b/go-zero.dev/en/resource/author.jpeg differ diff --git a/go-zero.dev/en/resource/b97bf7df-1781-436e-bf04-f1dd90c60537.png b/go-zero.dev/en/resource/b97bf7df-1781-436e-bf04-f1dd90c60537.png new file mode 100644 index 00000000..4e4455a3 Binary files /dev/null and b/go-zero.dev/en/resource/b97bf7df-1781-436e-bf04-f1dd90c60537.png differ diff --git a/go-zero.dev/en/resource/biz-redis-01.svg b/go-zero.dev/en/resource/biz-redis-01.svg new file mode 100644 index 00000000..95e80f43 --- /dev/null +++ b/go-zero.dev/en/resource/biz-redis-01.svg @@ -0,0 +1,16 @@ + + + + + + + clientbiz cachedb查 list1增、改、删 list2同步db \ No newline at end of file diff --git a/go-zero.dev/en/resource/biz-redis-02.svg b/go-zero.dev/en/resource/biz-redis-02.svg new file mode 100644 index 00000000..acf355e1 --- /dev/null +++ b/go-zero.dev/en/resource/biz-redis-02.svg @@ -0,0 +1,16 @@ + + + + + + + clientbiz cachedb增、删、查、改 list1查询 id = 1 的item2cache缓存未命中查询db回填缓存查询 id = 1 item3缓存命中 \ No newline at end of file diff --git a/go-zero.dev/en/resource/book.zip b/go-zero.dev/en/resource/book.zip new file mode 100644 index 00000000..a62c7bcd Binary files /dev/null and b/go-zero.dev/en/resource/book.zip differ diff --git a/go-zero.dev/en/resource/c42c34e8d33d48ec8a63e56feeae882a.png b/go-zero.dev/en/resource/c42c34e8d33d48ec8a63e56feeae882a.png new file mode 100644 index 00000000..1fdebc4b Binary files /dev/null and b/go-zero.dev/en/resource/c42c34e8d33d48ec8a63e56feeae882a.png differ diff --git a/go-zero.dev/en/resource/ci-cd.png b/go-zero.dev/en/resource/ci-cd.png new file mode 100644 index 00000000..ee16b01e Binary files /dev/null and b/go-zero.dev/en/resource/ci-cd.png differ diff --git a/go-zero.dev/en/resource/clone.png b/go-zero.dev/en/resource/clone.png new file mode 100644 index 00000000..e463d566 Binary files /dev/null and b/go-zero.dev/en/resource/clone.png differ diff --git a/go-zero.dev/en/resource/compare.png b/go-zero.dev/en/resource/compare.png new file mode 100644 index 00000000..42216ae3 Binary files /dev/null and b/go-zero.dev/en/resource/compare.png differ diff --git a/go-zero.dev/en/resource/dc500acd526d40aabfe4f53cf5bd180a_tplv-k3u1fbpfcp-zoom-1.png b/go-zero.dev/en/resource/dc500acd526d40aabfe4f53cf5bd180a_tplv-k3u1fbpfcp-zoom-1.png new file mode 100644 index 00000000..716ea11d Binary files /dev/null and b/go-zero.dev/en/resource/dc500acd526d40aabfe4f53cf5bd180a_tplv-k3u1fbpfcp-zoom-1.png differ diff --git a/go-zero.dev/en/resource/doc-edit.png b/go-zero.dev/en/resource/doc-edit.png new file mode 100644 index 00000000..14b9eb10 Binary files /dev/null and b/go-zero.dev/en/resource/doc-edit.png differ diff --git a/go-zero.dev/en/resource/docker_env.png b/go-zero.dev/en/resource/docker_env.png new file mode 100644 index 00000000..5ac04759 Binary files /dev/null and b/go-zero.dev/en/resource/docker_env.png differ diff --git a/go-zero.dev/en/resource/f93c621571074e44a2d403aa25e7db6f_tplv-k3u1fbpfcp-zoom-1.png b/go-zero.dev/en/resource/f93c621571074e44a2d403aa25e7db6f_tplv-k3u1fbpfcp-zoom-1.png new file mode 100644 index 00000000..afcde0fb Binary files /dev/null and b/go-zero.dev/en/resource/f93c621571074e44a2d403aa25e7db6f_tplv-k3u1fbpfcp-zoom-1.png differ diff --git a/go-zero.dev/en/resource/fork.png b/go-zero.dev/en/resource/fork.png new file mode 100644 index 00000000..44521bb3 Binary files /dev/null and b/go-zero.dev/en/resource/fork.png differ diff --git a/go-zero.dev/en/resource/fx_log.png b/go-zero.dev/en/resource/fx_log.png new file mode 100644 index 00000000..45ca95e6 Binary files /dev/null and b/go-zero.dev/en/resource/fx_log.png differ diff --git a/go-zero.dev/en/resource/gitlab-git-url.png b/go-zero.dev/en/resource/gitlab-git-url.png new file mode 100644 index 00000000..b2ed854a Binary files /dev/null and b/go-zero.dev/en/resource/gitlab-git-url.png differ diff --git a/go-zero.dev/en/resource/go-zero-logo.png b/go-zero.dev/en/resource/go-zero-logo.png new file mode 100644 index 00000000..a0ec1cd5 Binary files /dev/null and b/go-zero.dev/en/resource/go-zero-logo.png differ diff --git a/go-zero.dev/en/resource/go-zero-practise.png b/go-zero.dev/en/resource/go-zero-practise.png new file mode 100755 index 00000000..0be4da53 Binary files /dev/null and b/go-zero.dev/en/resource/go-zero-practise.png differ diff --git a/go-zero.dev/en/resource/go_live_template.png b/go-zero.dev/en/resource/go_live_template.png new file mode 100644 index 00000000..28110974 Binary files /dev/null and b/go-zero.dev/en/resource/go_live_template.png differ diff --git a/go-zero.dev/en/resource/goctl-api-select.png b/go-zero.dev/en/resource/goctl-api-select.png new file mode 100644 index 00000000..d852489f Binary files /dev/null and b/go-zero.dev/en/resource/goctl-api-select.png differ diff --git a/go-zero.dev/en/resource/goctl-api.png b/go-zero.dev/en/resource/goctl-api.png new file mode 100644 index 00000000..83a5cf48 Binary files /dev/null and b/go-zero.dev/en/resource/goctl-api.png differ diff --git a/go-zero.dev/en/resource/goctl-command.png b/go-zero.dev/en/resource/goctl-command.png new file mode 100644 index 00000000..6f38d223 Binary files /dev/null and b/go-zero.dev/en/resource/goctl-command.png differ diff --git a/go-zero.dev/en/resource/grafana-app.png b/go-zero.dev/en/resource/grafana-app.png new file mode 100644 index 00000000..aa97e4d5 Binary files /dev/null and b/go-zero.dev/en/resource/grafana-app.png differ diff --git a/go-zero.dev/en/resource/grafana-panel.png b/go-zero.dev/en/resource/grafana-panel.png new file mode 100644 index 00000000..b82430c5 Binary files /dev/null and b/go-zero.dev/en/resource/grafana-panel.png differ diff --git a/go-zero.dev/en/resource/grafana-qps.png b/go-zero.dev/en/resource/grafana-qps.png new file mode 100644 index 00000000..14a86dd5 Binary files /dev/null and b/go-zero.dev/en/resource/grafana-qps.png differ diff --git a/go-zero.dev/en/resource/grafana.png b/go-zero.dev/en/resource/grafana.png new file mode 100644 index 00000000..0c648728 Binary files /dev/null and b/go-zero.dev/en/resource/grafana.png differ diff --git a/go-zero.dev/en/resource/handler.gif b/go-zero.dev/en/resource/handler.gif new file mode 100644 index 00000000..fd1c1c38 Binary files /dev/null and b/go-zero.dev/en/resource/handler.gif differ diff --git a/go-zero.dev/en/resource/info.gif b/go-zero.dev/en/resource/info.gif new file mode 100644 index 00000000..f4c26bf5 Binary files /dev/null and b/go-zero.dev/en/resource/info.gif differ diff --git a/go-zero.dev/en/resource/intellij-model.png b/go-zero.dev/en/resource/intellij-model.png new file mode 100644 index 00000000..66dd40ad Binary files /dev/null and b/go-zero.dev/en/resource/intellij-model.png differ diff --git a/go-zero.dev/en/resource/jenkins-add-credentials.png b/go-zero.dev/en/resource/jenkins-add-credentials.png new file mode 100644 index 00000000..e5dc389d Binary files /dev/null and b/go-zero.dev/en/resource/jenkins-add-credentials.png differ diff --git a/go-zero.dev/en/resource/jenkins-build-with-parameters.png b/go-zero.dev/en/resource/jenkins-build-with-parameters.png new file mode 100644 index 00000000..14684389 Binary files /dev/null and b/go-zero.dev/en/resource/jenkins-build-with-parameters.png differ diff --git a/go-zero.dev/en/resource/jenkins-choice.png b/go-zero.dev/en/resource/jenkins-choice.png new file mode 100644 index 00000000..c86e722a Binary files /dev/null and b/go-zero.dev/en/resource/jenkins-choice.png differ diff --git a/go-zero.dev/en/resource/jenkins-configure.png b/go-zero.dev/en/resource/jenkins-configure.png new file mode 100644 index 00000000..69a0b430 Binary files /dev/null and b/go-zero.dev/en/resource/jenkins-configure.png differ diff --git a/go-zero.dev/en/resource/jenkins-credentials-id.png b/go-zero.dev/en/resource/jenkins-credentials-id.png new file mode 100644 index 00000000..2f633628 Binary files /dev/null and b/go-zero.dev/en/resource/jenkins-credentials-id.png differ diff --git a/go-zero.dev/en/resource/jenkins-credentials.png b/go-zero.dev/en/resource/jenkins-credentials.png new file mode 100644 index 00000000..372b9262 Binary files /dev/null and b/go-zero.dev/en/resource/jenkins-credentials.png differ diff --git a/go-zero.dev/en/resource/jenkins-git.png b/go-zero.dev/en/resource/jenkins-git.png new file mode 100644 index 00000000..950fc5b6 Binary files /dev/null and b/go-zero.dev/en/resource/jenkins-git.png differ diff --git a/go-zero.dev/en/resource/jenkins-new-item.png b/go-zero.dev/en/resource/jenkins-new-item.png new file mode 100644 index 00000000..4ad817b6 Binary files /dev/null and b/go-zero.dev/en/resource/jenkins-new-item.png differ diff --git a/go-zero.dev/en/resource/json_tag.png b/go-zero.dev/en/resource/json_tag.png new file mode 100644 index 00000000..e8f44c9c Binary files /dev/null and b/go-zero.dev/en/resource/json_tag.png differ diff --git a/go-zero.dev/en/resource/jump.gif b/go-zero.dev/en/resource/jump.gif new file mode 100644 index 00000000..8581aae5 Binary files /dev/null and b/go-zero.dev/en/resource/jump.gif differ diff --git a/go-zero.dev/en/resource/k8s-01.png b/go-zero.dev/en/resource/k8s-01.png new file mode 100644 index 00000000..9192926a Binary files /dev/null and b/go-zero.dev/en/resource/k8s-01.png differ diff --git a/go-zero.dev/en/resource/k8s-02.png b/go-zero.dev/en/resource/k8s-02.png new file mode 100644 index 00000000..2ec67d6d Binary files /dev/null and b/go-zero.dev/en/resource/k8s-02.png differ diff --git a/go-zero.dev/en/resource/k8s-03.png b/go-zero.dev/en/resource/k8s-03.png new file mode 100644 index 00000000..e672db3a Binary files /dev/null and b/go-zero.dev/en/resource/k8s-03.png differ diff --git a/go-zero.dev/en/resource/live_template.gif b/go-zero.dev/en/resource/live_template.gif new file mode 100644 index 00000000..dac3499e Binary files /dev/null and b/go-zero.dev/en/resource/live_template.gif differ diff --git a/go-zero.dev/en/resource/log-flow.png b/go-zero.dev/en/resource/log-flow.png new file mode 100644 index 00000000..2421ddce Binary files /dev/null and b/go-zero.dev/en/resource/log-flow.png differ diff --git a/go-zero.dev/en/resource/log.png b/go-zero.dev/en/resource/log.png new file mode 100644 index 00000000..9c751d7e Binary files /dev/null and b/go-zero.dev/en/resource/log.png differ diff --git a/go-zero.dev/en/resource/logo.png b/go-zero.dev/en/resource/logo.png new file mode 100644 index 00000000..16798ee5 Binary files /dev/null and b/go-zero.dev/en/resource/logo.png differ diff --git a/go-zero.dev/en/resource/new_pr.png b/go-zero.dev/en/resource/new_pr.png new file mode 100644 index 00000000..02d38b9f Binary files /dev/null and b/go-zero.dev/en/resource/new_pr.png differ diff --git a/go-zero.dev/en/resource/pipeline.png b/go-zero.dev/en/resource/pipeline.png new file mode 100644 index 00000000..eb51eaec Binary files /dev/null and b/go-zero.dev/en/resource/pipeline.png differ diff --git a/go-zero.dev/en/resource/pr_record.png b/go-zero.dev/en/resource/pr_record.png new file mode 100644 index 00000000..8b0e4937 Binary files /dev/null and b/go-zero.dev/en/resource/pr_record.png differ diff --git a/go-zero.dev/en/resource/project_generate_code.png b/go-zero.dev/en/resource/project_generate_code.png new file mode 100644 index 00000000..a637403b Binary files /dev/null and b/go-zero.dev/en/resource/project_generate_code.png differ diff --git a/go-zero.dev/en/resource/prometheus-flow.png b/go-zero.dev/en/resource/prometheus-flow.png new file mode 100644 index 00000000..ce6a97af Binary files /dev/null and b/go-zero.dev/en/resource/prometheus-flow.png differ diff --git a/go-zero.dev/en/resource/prometheus-graph.webp b/go-zero.dev/en/resource/prometheus-graph.webp new file mode 100644 index 00000000..d283fc5a Binary files /dev/null and b/go-zero.dev/en/resource/prometheus-graph.webp differ diff --git a/go-zero.dev/en/resource/prometheus-start.png b/go-zero.dev/en/resource/prometheus-start.png new file mode 100644 index 00000000..ee0bbefa Binary files /dev/null and b/go-zero.dev/en/resource/prometheus-start.png differ diff --git a/go-zero.dev/en/resource/psiTree.png b/go-zero.dev/en/resource/psiTree.png new file mode 100644 index 00000000..06af982a Binary files /dev/null and b/go-zero.dev/en/resource/psiTree.png differ diff --git a/go-zero.dev/en/resource/redis-cache-01.png b/go-zero.dev/en/resource/redis-cache-01.png new file mode 100644 index 00000000..f07a1133 Binary files /dev/null and b/go-zero.dev/en/resource/redis-cache-01.png differ diff --git a/go-zero.dev/en/resource/redis-cache-02.png b/go-zero.dev/en/resource/redis-cache-02.png new file mode 100644 index 00000000..ba8f2fb0 Binary files /dev/null and b/go-zero.dev/en/resource/redis-cache-02.png differ diff --git a/go-zero.dev/en/resource/redis-cache-03.png b/go-zero.dev/en/resource/redis-cache-03.png new file mode 100644 index 00000000..8b2449a8 Binary files /dev/null and b/go-zero.dev/en/resource/redis-cache-03.png differ diff --git a/go-zero.dev/en/resource/redis-cache-04.png b/go-zero.dev/en/resource/redis-cache-04.png new file mode 100644 index 00000000..38b3f0f4 Binary files /dev/null and b/go-zero.dev/en/resource/redis-cache-04.png differ diff --git a/go-zero.dev/en/resource/redis-cache-05.png b/go-zero.dev/en/resource/redis-cache-05.png new file mode 100644 index 00000000..4a743ec7 Binary files /dev/null and b/go-zero.dev/en/resource/redis-cache-05.png differ diff --git a/go-zero.dev/en/resource/redis-cache-06.png b/go-zero.dev/en/resource/redis-cache-06.png new file mode 100644 index 00000000..ec9cf88f Binary files /dev/null and b/go-zero.dev/en/resource/redis-cache-06.png differ diff --git a/go-zero.dev/en/resource/redis-cache-07.png b/go-zero.dev/en/resource/redis-cache-07.png new file mode 100644 index 00000000..c84d292d Binary files /dev/null and b/go-zero.dev/en/resource/redis-cache-07.png differ diff --git a/go-zero.dev/en/resource/redis-cache-08.png b/go-zero.dev/en/resource/redis-cache-08.png new file mode 100644 index 00000000..039816e4 Binary files /dev/null and b/go-zero.dev/en/resource/redis-cache-08.png differ diff --git a/go-zero.dev/en/resource/redis-cache-09.webp b/go-zero.dev/en/resource/redis-cache-09.webp new file mode 100644 index 00000000..e13122b6 Binary files /dev/null and b/go-zero.dev/en/resource/redis-cache-09.webp differ diff --git a/go-zero.dev/en/resource/redis-cache-10.png b/go-zero.dev/en/resource/redis-cache-10.png new file mode 100644 index 00000000..ec8e69a3 Binary files /dev/null and b/go-zero.dev/en/resource/redis-cache-10.png differ diff --git a/go-zero.dev/en/resource/redis-cache-11.webp b/go-zero.dev/en/resource/redis-cache-11.webp new file mode 100644 index 00000000..5476baf0 Binary files /dev/null and b/go-zero.dev/en/resource/redis-cache-11.webp differ diff --git a/go-zero.dev/en/resource/service.gif b/go-zero.dev/en/resource/service.gif new file mode 100644 index 00000000..dbf7f613 Binary files /dev/null and b/go-zero.dev/en/resource/service.gif differ diff --git a/go-zero.dev/en/resource/service.png b/go-zero.dev/en/resource/service.png new file mode 100644 index 00000000..09b51d7f Binary files /dev/null and b/go-zero.dev/en/resource/service.png differ diff --git a/go-zero.dev/en/resource/ssh-add-key.png b/go-zero.dev/en/resource/ssh-add-key.png new file mode 100644 index 00000000..70635b7e Binary files /dev/null and b/go-zero.dev/en/resource/ssh-add-key.png differ diff --git a/go-zero.dev/en/resource/type.gif b/go-zero.dev/en/resource/type.gif new file mode 100644 index 00000000..e4d4d7a1 Binary files /dev/null and b/go-zero.dev/en/resource/type.gif differ diff --git a/go-zero.dev/en/resource/user-pipeline-script.png b/go-zero.dev/en/resource/user-pipeline-script.png new file mode 100644 index 00000000..57ede338 Binary files /dev/null and b/go-zero.dev/en/resource/user-pipeline-script.png differ diff --git a/go-zero.dev/en/route-naming-spec.md b/go-zero.dev/en/route-naming-spec.md new file mode 100644 index 00000000..7c3b4407 --- /dev/null +++ b/go-zero.dev/en/route-naming-spec.md @@ -0,0 +1,13 @@ +# Route Rules +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +* Recommended spine naming +* Combinations of lowercase words and horizontal bars (-) +* What you see is what you get + +```go +/user/get-info +/user/get/info +/user/password/change/:id +``` \ No newline at end of file diff --git a/go-zero.dev/en/rpc-call.md b/go-zero.dev/en/rpc-call.md new file mode 100644 index 00000000..d9997867 --- /dev/null +++ b/go-zero.dev/en/rpc-call.md @@ -0,0 +1,256 @@ +# RPC Implement & Call +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +In a large system, there must be data transfer between multiple subsystems (services). If there is data transfer, you need a communication method. You can choose the simplest http for communication or rpc service for communication. +In go-zero, we use zrpc to communicate between services, which is based on grpc. + +## Scenes +In the front, we have improved the interface protocol for user login, user query of books, etc., but the user did not do any user verification when querying the book. If the current user is a non-existent user, we do not allow him to view book information. +From the above information, we can know that the user service needs to provide a method to obtain user information for use by the search service, so we need to create a user rpc service and provide a getUser method. + +## rpc service writing + +* Compile the proto file + ```shell + $ vim service/user/cmd/rpc/user.proto + ``` + ```protobuf + syntax = "proto3"; + + package user; + + option go_package = "user"; + + message IdReq{ + int64 id = 1; + } + + message UserInfoReply{ + int64 id = 1; + string name = 2; + string number = 3; + string gender = 4; + } + + service user { + rpc getUser(IdReq) returns(UserInfoReply); + } + ``` + * Generate rpc service code + ```shell + $ cd service/user/cmd/rpc + $ goctl rpc proto -src user.proto -dir . + ``` + +> [!TIPS] +> If the installed version of `protoc-gen-go` is larger than 1.4.0, it is recommended to add `go_package` to the proto file + +* Add configuration and improve yaml configuration items + ```shell + $ vim service/user/cmd/rpc/internal/config/config.go + ``` + ```go + type Config struct { + zrpc.RpcServerConf + Mysql struct { + DataSource string + } + CacheRedis cache.CacheConf + } + ``` + ```shell + $ vim /service/user/cmd/rpc/etc/user.yaml + ``` + ```yaml + Name: user.rpc + ListenOn: 127.0.0.1:8080 + Etcd: + Hosts: + - $etcdHost + Key: user.rpc + Mysql: + DataSource: $user:$password@tcp($url)/$db?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai + CacheRedis: + - Host: $host + Pass: $pass + Type: node + ``` + > [!TIP] + > $user: mysql database user + > + > $password: mysql database password + > + > $url: mysql database connection address + > + > $db: mysql database db name, that is, the database where the user table is located + > + > $host: Redis connection address Format: ip:port, such as: 127.0.0.1:6379 + > + > $pass: redis password + > + > $etcdHost: etcd connection address, format: ip:port, such as: 127.0.0.1:2379 + > + > For more configuration information, please refer to [rpc configuration introduction](rpc-config.md) + +* Add resource dependency + ```shell + $ vim service/user/cmd/rpc/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), + } + } + ``` +* Add rpc logic + ```shell + $ service/user/cmd/rpc/internal/logic/getuserlogic.go + ``` + ```go + func (l *GetUserLogic) GetUser(in *user.IdReq) (*user.UserInfoReply, error) { + one, err := l.svcCtx.UserModel.FindOne(in.Id) + if err != nil { + return nil, err + } + + return &user.UserInfoReply{ + Id: one.Id, + Name: one.Name, + Number: one.Number, + Gender: one.Gender, + }, nil + } + ``` + +## Use rpc +Next we call user rpc in the search service + +* Add UserRpc configuration and yaml configuration items + ```shell + $ vim service/search/cmd/api/internal/config/config.go + ``` + ```go + type Config struct { + rest.RestConf + Auth struct { + AccessSecret string + AccessExpire int64 + } + UserRpc zrpc.RpcClientConf + } + ``` + ```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 + UserRpc: + Etcd: + Hosts: + - $etcdHost + Key: user.rpc + ``` + > [!TIP] + > $AccessSecret: This value must be consistent with the one declared in the user api. + > + > $AccessExpire: Valid period + > + > $etcdHost: etcd connection address + > + > The `Key` in etcd must be consistent with the Key in the user rpc service configuration +* Add dependency + ```shell + $ vim service/search/cmd/api/internal/svc/servicecontext.go + ``` + ```go + type ServiceContext struct { + Config config.Config + Example rest.Middleware + UserRpc userclient.User + } + + func NewServiceContext(c config.Config) *ServiceContext { + return &ServiceContext{ + Config: c, + Example: middleware.NewExampleMiddleware().Handle, + UserRpc: userclient.NewUser(zrpc.MustNewClient(c.UserRpc)), + } + } + ``` +* Supplementary logic + ```shell + $ vim /service/search/cmd/api/internal/logic/searchlogic.go + ``` + ```go + func (l *SearchLogic) Search(req types.SearchReq) (*types.SearchReply, error) { + userIdNumber := json.Number(fmt.Sprintf("%v", l.ctx.Value("userId"))) + logx.Infof("userId: %s", userIdNumber) + userId, err := userIdNumber.Int64() + if err != nil { + return nil, err + } + + // use user rpc + _, err = l.svcCtx.UserRpc.GetUser(l.ctx, &userclient.IdReq{ + Id: userId, + }) + if err != nil { + return nil, err + } + + return &types.SearchReply{ + Name: req.Name, + Count: 100, + }, nil + } + ``` +## Start and verify the service +* Start etcd, redis, mysql +* Start user rpc + ```shell + $ cd /service/user/cmd/rpc + $ go run user.go -f etc/user.yaml + ``` + ```text + Starting rpc server at 127.0.0.1:8080... + ``` +* Start search api +```shell +$ cd service/search/cmd/api +$ go run search.go -f etc/search-api.yaml +``` + +* Verification Service + ```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: Tue, 09 Feb 2021 06:05:52 GMT + Content-Length: 32 + + {"name":"xiyouji","count":100} + ``` + +# Guess you wants +* [RPC Configuration](rpc-config.md) +* [RPC Directory Structure](rpc-dir.md) +* [RPC Commands](goctl-rpc.md) diff --git a/go-zero.dev/en/rpc-config.md b/go-zero.dev/en/rpc-config.md new file mode 100644 index 00000000..865ac746 --- /dev/null +++ b/go-zero.dev/en/rpc-config.md @@ -0,0 +1,55 @@ +# RPC Configuration +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + + +The rpc configuration controls various functions of an rpc service, including but not limited to listening address, etcd configuration, timeout, fuse configuration, etc. Below we will use a common rpc service configuration to illustrate. + +## Configuration instructions +```go +Config struct { + zrpc.RpcServerConf + CacheRedis cache.CacheConf // Redis cache configuration, see the api configuration instructions for details, and I won’t go into details here + Mysql struct { // mysql database access configuration, see api configuration instructions for details, not repeat here + DataSource string + } +} +``` + +### zrpc.RpcServerConf +```go +RpcServerConf struct { + service.ServiceConf // mysql database access configuration, see api configuration instructions for details, not repeat here + ListenOn string // rpc listening address and port, such as: 127.0.0.1:8888 + Etcd discov.EtcdConf `json:",optional"` // etcd related configuration + Auth bool `json:",optional"` // Whether to enable Auth, if yes, Redis is required + Redis redis.RedisKeyConf `json:",optional"` // Auth verification + StrictControl bool `json:",optional"` // Whether it is Strict mode, if it is, the error is Auth failure, otherwise it can be considered as successful + // pending forever is not allowed + // never set it to 0, if zero, the underlying will set to 2s automatically + Timeout int64 `json:",default=2000"` // Timeout control, unit: milliseconds + CpuThreshold int64 `json:",default=900,range=[0:1000]"` // CPU load reduction threshold, the default is 900, the allowable setting range is 0 to 1000 +} +``` + +### discov.EtcdConf +```go +type EtcdConf struct { + Hosts []string // etcd host array + Key string // rpc registration key +} +``` + +### redis.RedisKeyConf +```go +RedisConf struct { + Host string // redis host + Type string `json:",default=node,options=node|cluster"` // redis type + Pass string `json:",optional"` // redis password +} + +RedisKeyConf struct { + RedisConf + Key string `json:",optional"` // Verification key +} +``` diff --git a/go-zero.dev/en/rpc-dir.md b/go-zero.dev/en/rpc-dir.md new file mode 100644 index 00000000..6b0c2ca8 --- /dev/null +++ b/go-zero.dev/en/rpc-dir.md @@ -0,0 +1,51 @@ +# RPC Directory Structure +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +```text +. +├── etc // yaml configuration file +│ └── greet.yaml +├── go.mod +├── greet // pb.go folder① +│ └── greet.pb.go +├── greet.go // main entry +├── greet.proto // proto source file +├── greetclient // call logic ② +│ └── greet.go +└── internal + ├── config // yaml configuration corresponding entity + │ └── config.go + ├── logic // Business code + │ └── pinglogic.go + ├── server // rpc server + │ └── greetserver.go + └── svc // Dependent resources + └── servicecontext.go +``` + +> [!TIP] +> ① The name of the pb folder (the old version folder is fixed as pb) is taken from the value of option go_package in the proto file. The last level is converted according to a certain format. If there is no such declaration, it is taken from the value of package. The approximate code is as follows: + +```go + if option.Name == "go_package" { + ret.GoPackage = option.Constant.Source + } + ... + if len(ret.GoPackage) == 0 { + ret.GoPackage = ret.Package.Name + } + ret.PbPackage = GoSanitized(filepath.Base(ret.GoPackage)) + ... +``` +> [!TIP] +> For GoSanitized method, please refer to google.golang.org/protobuf@v1.25.0/internal/strs/strings.go:71 + +> [!TIP] +> ② The name of the call layer folder is taken from the name of the service in the proto. If the name of the sercice is equal to the name of the pb folder, the client will be added after service to distinguish between pb and call. + +```go +if strings.ToLower(proto.Service.Name) == strings.ToLower(proto.GoPackage) { + callDir = filepath.Join(ctx.WorkDir, strings.ToLower(stringx.From(proto.Service.Name+"_client").ToCamel())) +} +``` \ No newline at end of file diff --git a/go-zero.dev/en/service-deployment.md b/go-zero.dev/en/service-deployment.md new file mode 100644 index 00000000..6d30e123 --- /dev/null +++ b/go-zero.dev/en/service-deployment.md @@ -0,0 +1,243 @@ +# Service Deployment +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +This section uses jenkins to demonstrate a simple service deployment to k8s. + +## Prepare +* k8s cluster installation +* gitlab environment installation +* jenkins environment installation +* redis&mysql&nginx&etcd installation +* [goctl install](goctl-install.md) + +> [!TIP] +> Ensure that goctl is installed on each node of k8s +> +> Please google for the installation of the above environment, and I will not introduce it here. + +## Service deployment +### 1、Relevant preparations for gitlab code warehouse + +#### 1.1、Add SSH Key + +Enter gitlab, click on the user center, find `Settings`, find the `SSH Keys` tab on the left +![ssh key](./resource/ssh-add-key.png) + +* 1、View the public key on the machine where jenkins is located + +```shell +$ cat ~/.ssh/id_rsa.pub +``` + +* 2、If not, you need to generate it, if it exists, please skip to step 3 + +```shell +$ ssh-keygen -t rsa -b 2048 -C "email@example.com" +``` + +> "email@example.com" 可以替换为自己的邮箱 +> +After completing the generation, repeat the first step + +* 3、Add the public key to gitlab + +#### 1.2、Upload the code to the gitlab warehouse +Create a new project `go-zero-demo` and upload the code. Details are not described here. + +### 2、jenkins + +#### 2.1、Add credentials + +* View the private key of the machine where Jenkins is located, which corresponds to the previous gitlab public key + +```shell +$ cat id_rsa +``` + +* Enter jenkins, click on `Manage Jenkins`-> `Manage Credentials` + ![credentials](./resource/jenkins-credentials.png) + +* Go to the `Global Credentials` page, add credentials, `Username` is an identifier, add pipeline later, you know that this identifier represents the credentials of gitlab, and Private Key` is the private key obtained above + ![jenkins-add-credentials](./resource/jenkins-add-credentials.png) + +#### 2.2、 Add global variables +Enter `Manage Jenkins`->`Configure System`, slide to the entry of `Global Properties`, add docker private warehouse related information, as shown in the figure is `docker username`, `docker user password`, `docker private warehouse address` +![docker_server](./resource/docker_env.png) + +> [!TIP] +> +> `docker_user` your docker username +> +> `docker_pass` your docker user password +> +> `docker_server` your docker server +> +> The private warehouse I use here, if you don’t use the private warehouse provided by the cloud vendor, you can build a private warehouse yourself. I won’t go into details here, and you can google it yourself. + +#### 2.3、Configure git +Go to `Manage Jenkins`->`Global Tool Configureation`, find the Git entry, fill in the path of the git executable file of the machine where jenkins is located, if not, you need to download the Git plugin in the jenkins plugin management. +![jenkins-git](./resource/jenkins-git.png) + + +![jenkins-configure](./resource/jenkins-configure.png) +#### 2.4、 Add a pipeline + +> The pipeline is used to build the project, pull code from gitlab->generate Dockerfile->deploy to k8s are all done in this step, here is the demo environment, in order to ensure the smooth deployment process, +> Need to install jenkins on the machine where one of the nodes of the k8s cluster is located, I installed it on the master here. + +* Get the credential id Go to the credential page and find the credential id whose Username is `gitlab` + ![jenkins-credentials-id](./resource/jenkins-credentials-id.png) + +* Go to the jenkins homepage, click on `New Item`, the name is `user` + ![jenkins-add-item](./resource/jenkins-new-item.png) + +* View project git address + ![gitlab-git-url](./resource/gitlab-git-url.png) + +* Add the service type Choice Parameter, check `This project is parameterized in General`, + Click `Add parameter` and select `Choice Parameter`, add the selected value constant (api, rpc) and the variable (type) of the received value according to the figure, which will be used in the Pipeline script later. + ![jenkins-choice-parameter](./resource/jenkins-choice.png) + +* Configure `user`, on the `user` configuration page, swipe down to find `Pipeline script`, fill in the script content + +```text +pipeline { + agent any + parameters { + gitParameter name: 'branch', + type: 'PT_BRANCH', + branchFilter: 'origin/(.*)', + defaultValue: 'master', + selectedValue: 'DEFAULT', + sortMode: 'ASCENDING_SMART', + description: 'Select the branch' + } + + stages { + stage('service info') { + steps { + sh 'echo branch: $branch' + sh 'echo build service type:${JOB_NAME}-$type' + } + } + + + stage('check out') { + steps { + checkout([$class: 'GitSCM', + branches: [[name: '$branch']], + doGenerateSubmoduleConfigurations: false, + extensions: [], + submoduleCfg: [], + userRemoteConfigs: [[credentialsId: '${credentialsId}', url: '${gitUrl}']]]) + } + } + + stage('get commit_id') { + steps { + echo 'get commit_id' + git credentialsId: '${credentialsId}', url: '${gitUrl}' + script { + env.commit_id = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim() + } + } + } + + + stage('goctl version detection') { + steps{ + sh '/usr/local/bin/goctl -v' + } + } + + stage('Dockerfile Build') { + steps{ + sh '/usr/local/bin/goctl docker -go service/${JOB_NAME}/${type}/${JOB_NAME}.go' + script{ + env.image = sh(returnStdout: true, script: 'echo ${JOB_NAME}-${type}:${commit_id}').trim() + } + sh 'echo image:${image}' + sh 'docker build -t ${image} .' + } + } + + stage('Upload to the mirror warehouse') { + steps{ + sh '/root/dockerlogin.sh' + sh 'docker tag ${image} ${dockerServer}/${image}' + sh 'docker push ${dockerServer}/${image}' + } + } + + stage('Deploy to k8s') { + steps{ + script{ + env.deployYaml = sh(returnStdout: true, script: 'echo ${JOB_NAME}-${type}-deploy.yaml').trim() + env.port=sh(returnStdout: true, script: '/root/port.sh ${JOB_NAME}-${type}').trim() + } + + sh 'echo ${port}' + + sh 'rm -f ${deployYaml}' + sh '/usr/local/bin/goctl kube deploy -secret dockersecret -replicas 2 -nodePort 3${port} -requestCpu 200 -requestMem 50 -limitCpu 300 -limitMem 100 -name ${JOB_NAME}-${type} -namespace hey-go-zero -image ${dockerServer}/${image} -o ${deployYaml} -port ${port}' + sh '/usr/bin/kubectl apply -f ${deployYaml}' + } + } + + stage('Clean') { + steps{ + sh 'docker rmi -f ${image}' + sh 'docker rmi -f ${dockerServer}/${image}' + cleanWs notFailBuild: true + } + } + } +} +``` + +> [!TIP] +> ${credentialsId} should be replaced with your specific credential value, that is, a string of strings in the [Add Credentials] module, ${gitUrl} needs to be replaced with the git warehouse address of your code, other variables in the form of ${xxx} are not required Modify it and keep it as it is. +> ![user-pipepine-script](./resource/user-pipeline-script.png) + +### port.sh +``` +case $1 in +"user-api") echo 1000 +;; +"user-rpc") echo 1001 +;; +"course-api") echo 1002 +;; +"course-rpc") echo 1003 +;; +"selection-api") echo 1004 +esac +``` + +The content of dockerlogin.sh + +```shell +#!/bin/bash +docker login --username=$docker-user --password=$docker-pass $docker-server +``` + +* $docker-user: docker login username +* $docker-pass: docker login user password +* $docker-server: docker private address + +## View pipeline +![build with parameters](./resource/jenkins-build-with-parameters.png) +![build with parameters](./resource/pipeline.png) + +## View k8s service +![k8s01](./resource/k8s-01.png) + +# Guess you wants +* [Goctl Installation](goctl-install.md) +* [k8s](https://kubernetes.io/) +* [docker](https://www.docker.com/) +* [jenkins](https://www.jenkins.io/zh/doc/book/installing/) +* [jenkins pipeline](https://www.jenkins.io/zh/doc/pipeline/tour/hello-world/) +* [nginx](http://nginx.org/en/docs/) +* [etcd](https://etcd.io/docs/current/) \ No newline at end of file diff --git a/go-zero.dev/en/service-design.md b/go-zero.dev/en/service-design.md new file mode 100644 index 00000000..33501752 --- /dev/null +++ b/go-zero.dev/en/service-design.md @@ -0,0 +1,92 @@ +# Directory Structure +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +Directory splitting refers to the directory splitting in line with the best practices of go-zero, which is related to the splitting of microservices. In the best practice within the team, +We split a system into multiple subsystems according to the horizontal division of the business, and each subsystem should have an independent persistent storage and cache system. +For example, a shopping mall system needs to consist of a user system (user), a product management system (product), an order system (order), a shopping cart system (cart), a settlement center system (pay), an after-sale system (afterSale), etc. + +## System structure analysis +In the mall system mentioned above, while each system provides services to the outside (http), it also provides data to other subsystems for data access interfaces (rpc), so each subsystem can be split into a service , And provides two external ways to access the system, api and rpc, therefore, +The above system is divided into the following structure according to the directory structure: + +```text +. +├── afterSale +│   ├── api +│   └── rpc +├── cart +│   ├── api +│   └── rpc +├── order +│   ├── api +│   └── rpc +├── pay +│   ├── api +│   └── rpc +├── product +│   ├── api +│   └── rpc +└── user + ├── api + └── rpc +``` + +## rpc call chain suggestion +When designing the system, try to make the call between services one-way in the chain instead of cyclically. For example, the order service calls the user service, and the user service does not call the order service in turn. +When one of the services fails to start, it will affect each other and enter an infinite loop. You order to think it is caused by the user service failure, and the user thinks it is caused by the order service. If there are a large number of services in a mutual call chain, +You need to consider whether the service split is reasonable. + +## Directory structure of common service types +Among the above services, only api/rpc services are listed. In addition, there may be more service types under one service, such as rmq (message processing system), cron (timed task system), script (script), etc. , +Therefore, a service may contain the following directory structure: + +```text +user + ├── api // http access service, business requirement realization + ├── cronjob // Timed task, timed data update service + ├── rmq // Message processing system: mq and dq, handle some high concurrency and delay message services + ├── rpc // rpc service to provide basic data access to other subsystems + └── script // Script, handle some temporary operation requirements, and repair temporary data +``` + +## Example of complete project directory structure +```text +mall // 工程名称 +├── common // 通用库 +│   ├── randx +│   └── stringx +├── go.mod +├── go.sum +└── service // 服务存放目录 + ├── afterSale + │   ├── api + │   └── model + │   └── rpc + ├── cart + │   ├── api + │   └── model + │   └── rpc + ├── order + │   ├── api + │   └── model + │   └── rpc + ├── pay + │   ├── api + │   └── model + │   └── rpc + ├── product + │   ├── api + │   └── model + │   └── rpc + └── user + ├── api + ├── cronjob + ├── model + ├── rmq + ├── rpc + └── script +``` + +# Guess you wants +* [API Directory Structure](api-dir.md) diff --git a/go-zero.dev/en/service-monitor.md b/go-zero.dev/en/service-monitor.md new file mode 100644 index 00000000..f6caee4f --- /dev/null +++ b/go-zero.dev/en/service-monitor.md @@ -0,0 +1,106 @@ +# Monitor +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +In microservice governance, service monitoring is also a very important link. Monitoring whether a service is working normally needs to be carried out from multiple dimensions, such as:* mysql indicators +* mongo indicators +* redis indicator +* Request log +* Service index statistics +* Service health check +... + +The monitoring work is very large, and this section only uses the `service indicator monitoring` as an example for illustration. + +## Microservice indicator monitoring based on prometheus + +After the service is online, we often need to monitor the service so that we can find the problem early and make targeted optimization. The monitoring can be divided into various forms, such as log monitoring, call chain monitoring, indicator monitoring, and so on. Through indicator monitoring, the changing trend of service indicators can be clearly observed, and the operating status of the service can be understood, which plays a very important role in ensuring the stability of the service. + +Prometheus is an open source system monitoring and warning tool that supports a powerful query language, PromQL, allowing users to select and aggregate time series data in real time. Time series data is actively pulled by the server through the HTTP protocol, or it can be pushed through an intermediate gateway. Data, you can obtain monitoring targets through static configuration files or service discovery + +## Prometheus architecture + +The overall architecture and ecosystem components of Prometheus are shown in the following figure: +![prometheus-flow](./resource/prometheus-flow.png) + +Prometheus Server pulls monitoring indicators directly from the monitoring target or indirectly through the push gateway. It stores all captured sample data locally and executes a series of rules on this data to summarize and record new time series or existing data. Generate an alert. The monitoring data can be visualized through Grafana or other tools + +## go-zero service indicator monitoring based on prometheus + +The go-zero framework integrates prometheus-based service indicator monitoring. Below we use go-zero’s official example short url to demonstrate how to collect and monitor service indicators: +* The first step is to install Prometheus first, please refer to the official documentation for the installation steps +* go-zero does not enable prometheus monitoring by default. The opening method is very simple. You only need to add the following configuration to the shorturl-api.yaml file, where Host is the Prometheus Server address, which is a required configuration, the Port port is not filled in and the default is 9091, and the Path is used The path to pull metrics is /metrics by default + ```yaml + Prometheus: + Host: 127.0.0.1 + Port: 9091 + Path: /metrics + ``` + +* Edit the prometheus configuration file prometheus.yml, add the following configuration, and create targets.json + ```yaml + - job_name: 'file_ds' + file_sd_configs: + - files: + - targets.json + ``` +* Edit the targets.json file, where targets is the target address configured by shorturl, and add several default tags + ```yaml + [ + { + "targets": ["127.0.0.1:9091"], + "labels": { + "job": "shorturl-api", + "app": "shorturl-api", + "env": "test", + "instance": "127.0.0.1:8888" + } + } + ] + ``` +* Start the prometheus service, listening on port 9090 by default + ```shell + $ prometheus --config.file=prometheus.yml + ``` +* Enter `http://127.0.0.1:9090/` in the browser, and then click `Status` -> `Targets` to see the job whose status is Up, and the default label we configured can be seen in the Labels column +![prometheus-start](./resource/prometheus-start.png) + Through the above steps, we have completed the configuration work of Prometheus for the indicator monitoring collection of the shorturl service. For the sake of simplicity, we have performed manual configuration. In the actual production environment, we generally use the method of regularly updating configuration files or service discovery to configure monitoring. Goals, space is limited, not explained here, interested students please check the relevant documents on their own + +## Types of indicators monitored by go-zero + +go-zero currently adds monitoring of request metrics to the http middleware and rpc interceptor. + +Mainly from the two dimensions of request time and request error. The request time uses the Histogram metric type to define multiple Buckets to facilitate quantile statistics. The request error uses the Counter type, and the path tag rpc metric is added to the http metric. Added the method tag for detailed monitoring. +Next, demonstrate how to view monitoring indicators: + +First execute the following command multiple times on the command line + +```shell +$ curl -i "http://localhost:8888/shorten?url=http://www.xiaoheiban.cn" +``` +Open Prometheus and switch to the Graph interface, and enter the {path="/shorten"} command in the input box to view the monitoring indicators, as shown below: +![prometheus-graph](./resource/prometheus-graph.webp) + +We use PromQL grammar query to filter the indicators whose path is /shorten, and the results show the indicator name and indicator value. The code value in the http_server_requests_code_total indicator is the status code of http, 200 indicates that the request is successful, and http_server_requests_duration_ms_bucket separately counts the results of different buckets. , You can also see that all the indicators have added the default indicators we configured +The Console interface mainly displays the index results of the query. The Graph interface provides us with a simple graphical display interface. In the actual production environment, we generally use Grafana for graphical display. + +## grafana dashboard + +Grafana is a visualization tool with powerful functions and supports multiple data sources such as Prometheus, Elasticsearch, Graphite, etc. For simple installation, please refer to the official documentation. The default port of grafana is 3000. After installation, enter http://localhost:3000/ in the browser. , The default account and password are both admin. + +The following demonstrates how to draw the visual interface based on the above indicators: +Click on the left sidebar `Configuration`->`Data Source`->`Add data source` to add a data source, where the HTTP URL is the address of the data source +![grafana](./resource/grafana.png) + +Click on the left sidebar to add dashboard, and then add Variables to facilitate filtering for different tags, such as adding app variables to filter different services +![grafana-app](./resource/grafana-app.png) + +Enter the dashboard and click Add panel in the upper right corner to add a panel to count the qps of the interface in the path dimension +![grafana-app](./resource/grafana-qps.png) + +The final effect is shown below. Different services can be filtered by service name. The panel shows the trend of qps with path /shorten. +![grafana-app](./resource/grafana-panel.png) + +# Summary + +The above demonstrates the simple process of go-zero based on prometheus+grafana service indicator monitoring. In the production environment, different dimensions of monitoring and analysis can be done according to the actual scenario. Now go-zero's monitoring indicators are mainly for http and rpc, which is obviously insufficient for the overall monitoring of the service, such as the monitoring of container resources, the monitoring of dependent mysql, redis and other resources, and the monitoring of custom indicators, etc. Go-zero will continue to optimize in this regard. Hope this article can help you \ No newline at end of file diff --git a/go-zero.dev/en/shorturl-en.md b/go-zero.dev/en/shorturl-en.md new file mode 100644 index 00000000..fb67d770 --- /dev/null +++ b/go-zero.dev/en/shorturl-en.md @@ -0,0 +1,543 @@ +# Rapid development of microservices + +English | [简体中文](../cn/shorturl.md) + +## 0. Why building microservices are so difficult + +To build a well working microservice, we need lots of knowledges from different aspects. + +* basic functionalities + 1. concurrency control and rate limit, to avoid being brought down by unexpected inbound + 2. service discovery, make sure new or terminated nodes are detected asap + 3. load balancing, balance the traffic base on the throughput of nodes + 4. timeout control, avoid the nodes continue to process the timed out requests + 5. circuit breaker, load shedding, fail fast, protects the failure nodes to recover asap + +* advanced functionalities + 1. authorization, make sure users can only access their own data + 2. tracing, to understand the whole system and locate the specific problem quickly + 3. logging, collects data and helps to backtrace problems + 4. observability, no metrics, no optimization + +For any point listed above, we need a long article to describe the theory and the implementation. But for us, the developers, it’s very difficult to understand all the concepts and make it happen in our systems. Although, we can use the frameworks that have been well served busy sites. [go-zero](https://github.com/zeromicro/go-zero) is born for this purpose, especially for cloud-native microservice systems. + +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`. + +Let’s 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, you’ll find that it’s so easy to write microservices! + +## 1. What is a shorturl service + +A shorturl service is that it converts a long url into a short one, by well designed algorithms. + +Writting this shorturl service is to demonstrate the complete flow of creating a microservice by using go-zero. But algorithms and detail implementations are quite simplified, and this shorturl service is not suitable for production use. + +## 2. Architecture of shorturl microservice + +Architecture + +* In this tutorial, I only use one rpc service, transform, to demonstrate. It’s not telling that one API Gateway only can call one RPC service, it’s only for simplicity here. +* In production, we should try best to isolate the data belongs to services, that means each service should only use its own database. + +## 3. goctl generated code overview + +All modules with green background are generated, and will be enabled when necessary. The modules with red background are handwritten code, which is typically business logic code. + +* API Gateway + + api + +* RPC + + rpc + +* model + + model + +And now, let’s walk through the complete flow of quickly create a microservice with go-zero. + +## 4. Get started + +* install etcd, mysql, redis + +* install protoc-gen-go + + ``` + go get -u github.com/golang/protobuf/protoc-gen-go@v1.3.2 + ``` + +* install goctl + + ```shell + GO111MODULE=on go get -u github.com/tal-tech/go-zero/tools/goctl + ``` + +* create the working dir `shorturl` and `shorturl/api` + +* in `shorturl` dir, execute `go mod init shorturl` to initialize `go.mod` + +## 5. Write code for API Gateway + +* use goctl to generate `api/shorturl.api` + + ```shell + goctl api -o shorturl.api + ``` + + for simplicity, the leading `info` block is removed, and the code looks like: + + ```go + type ( + expandReq { + shorten string `form:"shorten"` + } + + expandResp { + url string `json:"url"` + } + ) + + type ( + shortenReq { + url string `form:"url"` + } + + shortenResp { + shorten string `json:"shorten"` + } + ) + + service shorturl-api { + @server( + handler: ShortenHandler + ) + get /shorten(shortenReq) returns(shortenResp) + + @server( + handler: ExpandHandler + ) + get /expand(expandReq) returns(expandResp) + } + ``` + + the usage of `type` keyword is the same as that in go, service is used to define get/post/head/delete api requests, described below: + + * `service shorturl-api {` defines the service name + * `@server` defines the properties that used in server side + * `handler` defines the handler name + * `get /shorten(shortenReq) returns(shortenResp)` defines this is a GET request, the request parameters, and the response parameters + +* generate the code for API Gateway by using goctl + + ```shell + goctl api go -api shorturl.api -dir . + ``` + + the generated file structure looks like: + + ```Plain Text + . + ├── api + │   ├── etc + │   │   └── shorturl-api.yaml // configuration file + │   ├── internal + │   │   ├── config + │   │   │   └── config.go // configuration definition + │   │   ├── handler + │   │   │   ├── expandhandler.go // implements expandHandler + │   │   │   ├── routes.go // routes definition + │   │   │   └── shortenhandler.go // implements shortenHandler + │   │   ├── logic + │   │   │   ├── expandlogic.go // implements ExpandLogic + │   │   │   └── shortenlogic.go // implements ShortenLogic + │   │   ├── svc + │   │   │   └── servicecontext.go // defines ServiceContext + │   │   └── types + │   │   └── types.go // defines request/response + │   ├── shorturl.api + │   └── shorturl.go // main entrance + ├── go.mod + └── go.sum + ``` + +* start API Gateway service, listens on port 8888 by default + + ```shell + go run shorturl.go -f etc/shorturl-api.yaml + ``` + +* test API Gateway service + + ```shell + curl -i "http://localhost:8888/shorten?url=http://www.xiaoheiban.cn" + ``` + + response like: + + ```http + HTTP/1.1 200 OK + Content-Type: application/json + Date: Thu, 27 Aug 2020 14:31:39 GMT + Content-Length: 15 + + {"shortUrl":""} + ``` + + You can see that the API Gateway service did nothing except returned a zero value. And let’s implement the business logic in rpc service. + +* you can modify `internal/svc/servicecontext.go` to pass dependencies if needed + +* implement logic in package `internal/logic` + +* you can use goctl to generate code for clients base on the .api file + +* till now, the client engineer can work with the api, don’t need to wait for the implementation of server side + +## 6. Write code for transform rpc service + +- under directory `shorturl` create dir `rpc` + +* under directory `rpc/transform` create `transform.proto` file + + ```shell + goctl rpc template -o transform.proto + ``` + + edit the file and make the code looks like: + + ```protobuf + syntax = "proto3"; + + package transform; + + message expandReq { + string shorten = 1; + } + + message expandResp { + string url = 1; + } + + message shortenReq { + string url = 1; + } + + message shortenResp { + string shorten = 1; + } + + service transformer { + rpc expand(expandReq) returns(expandResp); + rpc shorten(shortenReq) returns(shortenResp); + } + ``` + +* use goctl to generate the rpc code, execute the following command in `rpc/transofrm` + + ```shell + goctl rpc proto -src transform.proto -dir . + ``` + + the generated file structure looks like: + + ```Plain Text + rpc/transform + ├── etc + │   └── transform.yaml // configuration file + ├── internal + │   ├── config + │   │   └── config.go // configuration definition + │   ├── logic + │   │   ├── expandlogic.go // implements expand logic + │   │   └── shortenlogic.go // implements shorten logic + │   ├── server + │   │   └── transformerserver.go // rpc handler + │   └── svc + │   └── servicecontext.go // defines service context, like dependencies + ├── pb + │   └── transform.pb.go + ├── transform.go // rpc main entrance + ├── transform.proto + └── transformer + ├── transformer.go // defines how rpc clients call this service + ├── transformer_mock.go // mock file, for test purpose + └── types.go // request/response definition + ``` + + just run it, looks like: + + ```shell + $ go run transform.go -f etc/transform.yaml + Starting rpc server at 127.0.0.1:8080... + ``` + + you can change the listening port in file `etc/transform.yaml`. + +## 7. Modify API Gateway to call transform rpc service + +* modify the configuration file `shorturl-api.yaml`, add the following: + + ```yaml + Transform: + Etcd: + Hosts: + - localhost:2379 + Key: transform.rpc + ``` + + automatically discover the transform service by using etcd. + +* modify the file `internal/config/config.go`, add dependency on transform service: + + ```go + type Config struct { + rest.RestConf + Transform zrpc.RpcClientConf // manual code + } + ``` + +* modify the file `internal/svc/servicecontext.go`, like below: + + ```go + type ServiceContext struct { + Config config.Config + Transformer transformer.Transformer // manual code + } + + func NewServiceContext(c config.Config) *ServiceContext { + return &ServiceContext{ + Config: c, + Transformer: transformer.NewTransformer(zrpc.MustNewClient(c.Transform)), // manual code + } + } + ``` + + passing the dependencies among services within ServiceContext. + +* modify the method `Expand` in the file `internal/logic/expandlogic.go`, looks like: + + ```go + func (l *ExpandLogic) Expand(req types.ExpandReq) (*types.ExpandResp, error) { + // manual code start + resp, err := l.svcCtx.Transformer.Expand(l.ctx, &transformer.ExpandReq{ + Shorten: req.Shorten, + }) + if err != nil { + return nil, err + } + + return &types.ExpandResp{ + Url: resp.Url, + }, nil + // manual code stop + } + ``` + + by calling the method `Expand` of `transformer` to restore the shortened url. + +* modify the file `internal/logic/shortenlogic.go`, looks like: + + ```go + func (l *ShortenLogic) Shorten(req types.ShortenReq) (*types.ShortenResp, error) { + // manual code start + resp, err := l.svcCtx.Transformer.Shorten(l.ctx, &transformer.ShortenReq{ + Url: req.Url, + }) + if err != nil { + return nil, err + } + + return &types.ShortenResp{ + Shorten: resp.Shorten, + }, nil + // manual code stop + } + ``` + + by calling the method `Shorten` of `transformer` to shorten the url. + +Till now, we’ve done the modification of API Gateway. All the manually added code are marked. + +## 8. Define the database schema, generate the code for CRUD+cache + +* under shorturl, create the directory `rpc/transform/model`: `mkdir -p rpc/transform/model` + +* under the directory rpc/transform/model create the file called shorturl.sql`, contents as below: + + ```sql + CREATE TABLE `shorturl` + ( + `shorten` varchar(255) NOT NULL COMMENT 'shorten key', + `url` varchar(255) NOT NULL COMMENT 'original url', + PRIMARY KEY(`shorten`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + ``` + +* create DB and table + + ```sql + create database gozero; + ``` + + ```sql + source shorturl.sql; + ``` + +* under the directory `rpc/transform/model` execute the following command to genrate CRUD+cache code, `-c` means using `redis cache` + + ```shell + goctl model mysql ddl -c -src shorturl.sql -dir . + ``` + + you can also generate the code from the database url by using `datasource` subcommand instead of `ddl` + + the generated file structure looks like: + + ```Plain Text + rpc/transform/model + ├── shorturl.sql + ├── shorturlmodel.go // CRUD+cache code + └── vars.go // const and var definition + ``` + +## 9. Modify shorten/expand rpc to call crud+cache + +* modify `rpc/transform/etc/transform.yaml`, add the following: + + ```yaml + DataSource: root:@tcp(localhost:3306)/gozero + Table: shorturl + Cache: + - Host: localhost:6379 + ``` + + you can use multiple redis as cache. redis node and cluster are both supported. + +* modify `rpc/transform/internal/config.go`, like below: + + ```go + type Config struct { + zrpc.RpcServerConf + DataSource string // manual code + Table string // manual code + Cache cache.CacheConf // manual code + } + ``` + + added the configuration for mysql and redis cache. + +* modify `rpc/transform/internal/svc/servicecontext.go`, like below: + + ```go + type ServiceContext struct { + c config.Config + Model model.ShorturlModel // manual code + } + + func NewServiceContext(c config.Config) *ServiceContext { + return &ServiceContext{ + c: c, + Model: model.NewShorturlModel(sqlx.NewMysql(c.DataSource), c.Cache, c.Table), // manual code + } + } + ``` + +* modify `rpc/transform/internal/logic/expandlogic.go`, like below: + + ```go + func (l *ExpandLogic) Expand(in *transform.ExpandReq) (*transform.ExpandResp, error) { + // manual code start + res, err := l.svcCtx.Model.FindOne(in.Shorten) + if err != nil { + return nil, err + } + + return &transform.ExpandResp{ + Url: res.Url, + }, nil + // manual code stop + } + ``` + +* modify `rpc/shorten/internal/logic/shortenlogic.go`, looks like: + + ```go + func (l *ShortenLogic) Shorten(in *transform.ShortenReq) (*transform.ShortenResp, error) { + // manual code start, generates shorturl + key := hash.Md5Hex([]byte(in.Url))[:6] + _, err := l.svcCtx.Model.Insert(model.Shorturl{ + Shorten: key, + Url: in.Url, + }) + if err != nil { + return nil, err + } + + return &transform.ShortenResp{ + Shorten: key, + }, nil + // manual code stop + } + ``` + + till now, we finished modifing the code, all the modified code is marked. + +## 10. Call shorten and expand services + +* call shorten api + + ```shell + curl -i "http://localhost:8888/shorten?url=http://www.xiaoheiban.cn" + ``` + + response like: + + ```http + HTTP/1.1 200 OK + Content-Type: application/json + Date: Sat, 29 Aug 2020 10:49:49 GMT + Content-Length: 21 + + {"shorten":"f35b2a"} + ``` + +* call expand api + + ```shell + curl -i "http://localhost:8888/expand?shorten=f35b2a" + ``` + + response like: + + ```http + HTTP/1.1 200 OK + Content-Type: application/json + Date: Sat, 29 Aug 2020 10:51:53 GMT + Content-Length: 34 + + {"url":"http://www.xiaoheiban.cn"} + ``` + +## 11. Benchmark + +Because benchmarking the write requests depends on the write throughput of mysql, we only benchmarked the expand api. We read the data from mysql and cache it in redis. I chose 100 hot keys hardcoded in shorten.lua to generate the benchmark. + +![Benchmark](images/shorturl-benchmark.png) + +as shown above, in my MacBook Pro, the QPS is like 30K+. + +## 12. Full code + +[https://github.com/zeromicro/zero-examples/tree/main/shorturl](https://github.com/zeromicro/zero-examples/tree/main/shorturl) + +## 13. Conclusion + +We always adhere to **prefer tools over conventions and documents**. + +go-zero is not only a framework, but also a tool to simplify and standardize the building of micoservice systems. + +We not only keep the framework simple, but also encapsulate the complexity into the framework. And the developers are free from building the difficult and boilerplate code. Then we get the rapid development and less failure. + +For the generated code by goctl, lots of microservice components are included, like concurrency control, adaptive circuit breaker, adaptive load shedding, auto cache control etc. And it’s easy to deal with the busy sites. + +If you have any ideas that can help us to improve the productivity, tell me any time! 👏 diff --git a/go-zero.dev/en/source.md b/go-zero.dev/en/source.md new file mode 100644 index 00000000..1f1d75d4 --- /dev/null +++ b/go-zero.dev/en/source.md @@ -0,0 +1,5 @@ +# Source Code +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +* [demo](https://github.com/zeromicro/go-zero-demo) \ No newline at end of file diff --git a/go-zero.dev/en/stream.md b/go-zero.dev/en/stream.md new file mode 100644 index 00000000..1184c529 --- /dev/null +++ b/go-zero.dev/en/stream.md @@ -0,0 +1,367 @@ +# Stream processing +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +Stream processing is a computer programming paradigm that allows given a data sequence (stream processing data source), a series of data operations (functions) are applied to each element in the stream. At the same time, stream processing tools can significantly improve programmers' development efficiency, allowing them to write effective, clean, and concise code. + +Streaming data processing is very common in our daily work. For example, we often record many business logs in business development. These logs are usually sent to Kafka first, and then written to elasticsearch by the Job consumption Kafka, and the logs are in progress. In the process of stream processing, logs are often processed, such as filtering invalid logs, doing some calculations and recombining logs, etc. The schematic diagram is as follows: +![fx_log.png](./resource/fx_log.png) +### fx +[go-zero](https://github.com/zeromicro/go-zero) is a full-featured microservice framework. There are many very useful tools built in the framework, including streaming data processing tools [fx ](https://github.com/zeromicro/go-zero/tree/master/core/fx), let’s use a simple example to understand the tool: +```go +package main + +import ( + "fmt" + "os" + "os/signal" + "syscall" + "time" + + "github.com/tal-tech/go-zero/core/fx" +) + +func main() { + ch := make(chan int) + + go inputStream(ch) + go outputStream(ch) + + c := make(chan os.Signal, 1) + signal.Notify(c, syscall.SIGTERM, syscall.SIGINT) + <-c +} + +func inputStream(ch chan int) { + count := 0 + for { + ch <- count + time.Sleep(time.Millisecond * 500) + count++ + } +} + +func outputStream(ch chan int) { + fx.From(func(source chan<- interface{}) { + for c := range ch { + source <- c + } + }).Walk(func(item interface{}, pipe chan<- interface{}) { + count := item.(int) + pipe <- count + }).Filter(func(item interface{}) bool { + itemInt := item.(int) + if itemInt%2 == 0 { + return true + } + return false + }).ForEach(func(item interface{}) { + fmt.Println(item) + }) +} +``` + + +The inputStream function simulates the generation of stream data, and the outputStream function simulates the process of stream data. The From function is the input of the stream. The Walk function concurrently acts on each item. The Filter function filters the item as true and keeps it as false. Keep, the ForEach function traverses and outputs each item element. + + +### Intermediate operations of streaming data processing + + +There may be many intermediate operations in the data processing of a stream, and each intermediate operation can act on the stream. Just like the workers on the assembly line, each worker will return to the processed new part after operating the part, and in the same way, after the intermediate operation of the flow processing is completed, it will also return to a new flow. +![7715f4b6-8739-41ac-8c8c-04d187172e9d.png](./resource/7715f4b6-8739-41ac-8c8c-04d187172e9d.png) +Intermediate operations of fx stream processing: + +| Operation function | Features | Input | +| --- | --- | --- | +| Distinct | Remove duplicate items | KeyFunc, return the key that needs to be deduplicated | +| Filter | Filter items that do not meet the conditions | FilterFunc, Option controls the amount of concurrency | +| Group | Group items | KeyFunc, group by key | +| Head | Take out the first n items and return to the new stream | int64 reserved number | +| Map | Object conversion | MapFunc, Option controls the amount of concurrency | +| Merge | Merge item into slice and generate new stream | | +| Reverse | Reverse item | | +| Sort | Sort items | LessFunc implements sorting algorithm | +| Tail | Similar to the Head function, n items form a new stream after being taken out | int64 reserved number | +| Walk | Act on each item | WalkFunc, Option controls the amount of concurrency | + + + +The following figure shows each step and the result of each step: + + +![3aefec98-56eb-45a6-a4b2-9adbdf4d63c0.png](./resource/3aefec98-56eb-45a6-a4b2-9adbdf4d63c0.png) + + +### Usage and principle analysis + + +#### From + + +Construct a stream through the From function and return the Stream, and the stream data is stored through the channel: + + +```go +// Example +s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0} +fx.From(func(source chan<- interface{}) { + for _, v := range s { + source <- v + } +}) + +// Source Code +func From(generate GenerateFunc) Stream { + source := make(chan interface{}) + + threading.GoSafe(func() { + defer close(source) + generate(source) + }) + + return Range(source) +} +``` + + +#### Filter + + +The Filter function provides the function of filtering items, FilterFunc defines the filtering logic true to retain the item, and false to not retain: + + +```go +// Example: Keep even numbers +s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0} +fx.From(func(source chan<- interface{}) { + for _, v := range s { + source <- v + } +}).Filter(func(item interface{}) bool { + if item.(int)%2 == 0 { + return true + } + return false +}) + +// Source Code +func (p Stream) Filter(fn FilterFunc, opts ...Option) Stream { + return p.Walk(func(item interface{}, pipe chan<- interface{}) { + // Execute the filter function true to retain, false to discard + if fn(item) { + pipe <- item + } + }, opts...) +} +``` + + +#### Group + + +Group groups the stream data. The key of the group needs to be defined. After the data is grouped, it is stored in the channel as slices: + + +```go +// Example Group according to the first character "g" or "p", if not, it will be divided into another group + ss := []string{"golang", "google", "php", "python", "java", "c++"} + fx.From(func(source chan<- interface{}) { + for _, s := range ss { + source <- s + } + }).Group(func(item interface{}) interface{} { + if strings.HasPrefix(item.(string), "g") { + return "g" + } else if strings.HasPrefix(item.(string), "p") { + return "p" + } + return "" + }).ForEach(func(item interface{}) { + fmt.Println(item) + }) +} + +// Source Code +func (p Stream) Group(fn KeyFunc) Stream { + // Define group storage map + groups := make(map[interface{}][]interface{}) + for item := range p.source { + // User-defined group key + key := fn(item) + // Group the same key into a group + groups[key] = append(groups[key], item) + } + + source := make(chan interface{}) + go func() { + for _, group := range groups { + // A group of data with the same key is written to the channel + source <- group + } + close(source) + }() + + return Range(source) +} +``` + + +#### Reverse + + +reverse can reverse the elements in the stream: + + +![7e0fd2b8-d4c1-4130-a216-a7d3d4301116.png](./resource/7e0fd2b8-d4c1-4130-a216-a7d3d4301116.png) + + +```go +// Example +fx.Just(1, 2, 3, 4, 5).Reverse().ForEach(func(item interface{}) { + fmt.Println(item) +}) + +// Source Code +func (p Stream) Reverse() Stream { + var items []interface{} + // Get the data in the stream + for item := range p.source { + items = append(items, item) + } + // Reversal algorithm + for i := len(items)/2 - 1; i >= 0; i-- { + opp := len(items) - 1 - i + items[i], items[opp] = items[opp], items[i] + } + + // Write stream + return Just(items...) +} +``` + + +#### Distinct + + +Distinct de-duplicates elements in the stream. De-duplication is commonly used in business development. It is often necessary to de-duplicate user IDs, etc.: + + +```go +// Example +fx.Just(1, 2, 2, 2, 3, 3, 4, 5, 6).Distinct(func(item interface{}) interface{} { + return item +}).ForEach(func(item interface{}) { + fmt.Println(item) +}) +// Output: 1,2,3,4,5,6 + +// Source Code +func (p Stream) Distinct(fn KeyFunc) Stream { + source := make(chan interface{}) + + threading.GoSafe(func() { + defer close(source) + // Deduplication is performed by key, and only one of the same key is kept + keys := make(map[interface{}]lang.PlaceholderType) + for item := range p.source { + key := fn(item) + // The key is not retained if it exists + if _, ok := keys[key]; !ok { + source <- item + keys[key] = lang.Placeholder + } + } + }) + + return Range(source) +} +``` + + +#### Walk + + +The concurrency of the Walk function works on each item in the stream. You can set the number of concurrency through WithWorkers. The default number of concurrency is 16, and the minimum number of concurrency is 1. If you set unlimitedWorkers to true, the number of concurrency is unlimited, but the number of concurrent writes in the stream is unlimited. The data is limited by defaultWorkers. In WalkFunc, users can customize the elements that are subsequently written to the stream, and can write multiple elements without writing: + + +```go +// Example +fx.Just("aaa", "bbb", "ccc").Walk(func(item interface{}, pipe chan<- interface{}) { + newItem := strings.ToUpper(item.(string)) + pipe <- newItem +}).ForEach(func(item interface{}) { + fmt.Println(item) +}) + +// Source Code +func (p Stream) walkLimited(fn WalkFunc, option *rxOptions) Stream { + pipe := make(chan interface{}, option.workers) + + go func() { + var wg sync.WaitGroup + pool := make(chan lang.PlaceholderType, option.workers) + + for { + // Control the number of concurrent + pool <- lang.Placeholder + item, ok := <-p.source + if !ok { + <-pool + break + } + + wg.Add(1) + go func() { + defer func() { + wg.Done() + <-pool + }() + // Acting on every element + fn(item, pipe) + }() + } + + // Wait for processing to complete + wg.Wait() + close(pipe) + }() + + return Range(pipe) +} +``` + + +### Concurrent processing + + +In addition to stream data processing, the fx tool also provides function concurrency. The realization of a function in microservices often requires multiple services. Concurrent processing dependence can effectively reduce dependency time and improve service performance. + + +![b97bf7df-1781-436e-bf04-f1dd90c60537.png](./resource/b97bf7df-1781-436e-bf04-f1dd90c60537.png) + + +```go +fx.Parallel(func() { + userRPC() +}, func() { + accountRPC() +}, func() { + orderRPC() +}) +``` + + +Note that when fx.Parallel performs dependency parallel processing, there will be no error return. If you need an error return, or a dependency error report needs to end the dependency request immediately, please use the [MapReduce](https://gocn.vip/topics/10941) tool To process. + + +### Summary + + +This article introduces the basic concepts of stream processing and the stream processing tool fx in go-zero. There are many stream processing scenarios in actual production. I hope this article can give you some inspiration and better response Stream processing scene at work. + + + + + + diff --git a/go-zero.dev/en/summary.md b/go-zero.dev/en/summary.md new file mode 100644 index 00000000..d0114eb4 --- /dev/null +++ b/go-zero.dev/en/summary.md @@ -0,0 +1,84 @@ +# Summary + +* [Introduction](README.md) +* [About Us](about-us.md) +* [Join Us](join-us.md) +* [Concepts](concept-introduction.md) +* [Quick Start](quick-start.md) + * [Monolithic Service](monolithic-service.md) + * [Micro Service](micro-service.md) +* [Framework Design](framework-design.md) + * [Go-Zero Design](go-zero-design.md) + * [Go-Zero Features](go-zero-features.md) + * [API IDL](api-grammar.md) + * [API Directory Structure](api-dir.md) + * [RPC Directory Structure](rpc-dir.md) +* [Project Development](project-dev.md) + * [Prepare](prepare.md) + * [Golang Installation](golang-install.md) + * [Go Module Configuration](gomod-config.md) + * [Goctl Installation](goctl-install.md) + * [protoc & protoc-gen-go Installation](protoc-install.md) + * [More](prepare-other.md) + * [Development Rules](dev-specification.md) + * [Naming Rules](naming-spec.md) + * [Route Rules](route-naming-spec.md) + * [Coding Rules](coding-spec.md) + * [Development Flow](dev-flow.md) + * [Configuration Introduction](config-introduction.md) + * [API Configuration](api-config.md) + * [RPC Configuration](rpc-config.md) + * [Business Development](business-dev.md) + * [Directory Structure](service-design.md) + * [Model Generation](model-gen.md) + * [API Coding](api-coding.md) + * [Business Coding](business-coding.md) + * [JWT](jwt.md) + * [Middleware](middleware.md) + * [RPC Implement & Call](rpc-call.md) + * [Error Handling](error-handle.md) + * [CI/CD](ci-cd.md) + * [Service Deployment](service-deployment.md) + * [Log Collection](log-collection.md) + * [Trace](trace.md) + * [Monitor](service-monitor.md) +* [Goctl](goctl.md) + * [Commands & Flags](goctl-commands.md) + * [API Commands](goctl-api.md) + * [RPC Commands](goctl-rpc.md) + * [Model Commands](goctl-model.md) + * [Plugin Commands](goctl-plugin.md) + * [More Commands](goctl-other.md) +* [Template](template-manage.md) + * [Command](template-cmd.md) + * [Custom](template.md) +* [Extended](extended-reading.md) + * [logx](logx.md) + * [bloom](bloom.md) + * [executors](executors.md) + * [fx](fx.md) + * [mysql](mysql.md) + * [redis-lock](redis-lock.md) + * [periodlimit](periodlimit.md) + * [tokenlimit](tokenlimit.md) + * [TimingWheel](timing-wheel.md) +* [Tools](tool-center.md) + * [Intellij Plugin](intellij.md) + * [VSCode Plugin](vscode.md) +* [Plugins](plugin-center.md) +* [Learning Resources](learning-resource.md) + * [Wechat](wechat.md) + * [Night](goreading.md) + * [OpenTalk](gotalk.md) +* [User Practise](practise.md) + * [Persistent layer cache](redis-cache.md) + * [Business layer cache](buiness-cache.md) + * [Queue](go-queue.md) + * [Middle Ground System](datacenter.md) + * [Stream Handler](stream.md) + * [Online Exchange](online-exchange.md) +* [Contributor](contributor.md) +* [Document Contribute](doc-contibute.md) +* [Error](error.md) +* [Source Code](source.md) + diff --git a/go-zero.dev/en/template-cmd.md b/go-zero.dev/en/template-cmd.md new file mode 100644 index 00000000..ec7429b7 --- /dev/null +++ b/go-zero.dev/en/template-cmd.md @@ -0,0 +1,113 @@ +# Template Operation + +Template is the basis of data-driven generation, all code (rest api, rpc, model, docker, kube) generation will rely on template. +By default, the template generator selects the in-memory template for generation, while for developers who need to modify the template, they need to drop the template and make template changes in the next code generation. +For developers who need to modify the templates, they need to modify the templates, and then the next time the code is generated, it will load the templates under the specified path to generate. + +## Help +```text +NAME: + goctl template - template operation + +USAGE: + goctl template command [command options] [arguments...] + +COMMANDS: + init initialize the all templates(force update) + clean clean the all cache templates + update update template of the target category to the latest + revert revert the target template to the latest + +OPTIONS: + --help, -h show help +``` + +## Init +```text +NAME: + goctl template init - initialize the all templates(force update) + +USAGE: + goctl template init [command options] [arguments...] + +OPTIONS: + --home value the goctl home path of the template +``` + +## Clean +```text +NAME: + goctl template clean - clean the all cache templates + +USAGE: + goctl template clean [command options] [arguments...] + +OPTIONS: + --home value the goctl home path of the template +``` + +## Update +```text +NAME: + goctl template update - update template of the target category to the latest + +USAGE: + goctl template update [command options] [arguments...] + +OPTIONS: + --category value, -c value the category of template, enum [api,rpc,model,docker,kube] + --home value the goctl home path of the template +``` + +## Revert +```text +NAME: + goctl template revert - revert the target template to the latest + +USAGE: + goctl template revert [command options] [arguments...] + +OPTIONS: + --category value, -c value the category of template, enum [api,rpc,model,docker,kube] + --name value, -n value the target file name of template + --home value the goctl home path of the template +``` + +> [!TIP] +> +> `--home` Specify the template storage path + +## Template loading + +You can specify the folder where the template is located by `--home` during code generation, and the commands that have been supported to specify the template directory are + +- `goctl api go` Details can be found in `goctl api go --help` for help +- `goctl docker` Details can be viewed with `goctl docker --help` +- `goctl kube` Details can be viewed with `goctl kube --help` +- `goctl rpc new` Details can be viewed with `goctl rpc new --help` +- `goctl rpc proto` Details can be viewed with `goctl rpc proto --help` +- `goctl model mysql ddl` Details can be viewed with `goctl model mysql ddl --help` +- `goctl model mysql datasource` Details can be viewed with `goctl model mysql datasource --help` +- `goctl model postgresql datasource` Details can be viewed with `goctl model mysql datasource --help` +- `goctl model mongo` Details can be viewed with `goctl model mongo --help` + +The default (when `--home` is not specified) is to read from the `$HOME/.goctl` directory. + +## Example +* Initialize the template to the specified `$HOME/template` directory +```text +$ goctl template init --home $HOME/template +``` + +```text +Templates are generated in /Users/anqiansong/template, edit on your risk! +``` + +* Greet rpc generation using `$HOME/template` template +```text +$ goctl rpc new greet --home $HOME/template +``` + +```text +Done +``` \ No newline at end of file diff --git a/go-zero.dev/en/template-manage.md b/go-zero.dev/en/template-manage.md new file mode 100644 index 00000000..acfd5251 --- /dev/null +++ b/go-zero.dev/en/template-manage.md @@ -0,0 +1,4 @@ +# Template + +- [Command](template-cmd.md) +- [Custom](template.md) \ No newline at end of file diff --git a/go-zero.dev/en/template.md b/go-zero.dev/en/template.md new file mode 100644 index 00000000..7e122e31 --- /dev/null +++ b/go-zero.dev/en/template.md @@ -0,0 +1,160 @@ +# Template Modification + +## Scenario +Implement a uniformly formatted body accordingly, in the following format. +```json +{ + "code": 0, + "msg": "OK", + "data": {}// ① +} +``` + +① 实际相应数据 + +> [!TIP] +> The code generated by `go-zero` does not process it + +## Preparation +We go ahead and write a `Response` method in the `response` package under the project with `module` as `greet`, with a directory tree similar to the following. +```text +greet +├── reponse +│   └── response.go +└── xxx... +``` + +The code is as follows +```go +package reponse + +import ( + "net/http" + + "github.com/tal-tech/go-zero/rest/httpx" +) + +type Body struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data interface{} `json:"data,omitempty"` +} + +func Response(w http.ResponseWriter, resp interface{}, err error) { + var body Body + if err != nil { + body.Code = -1 + body.Msg = err.Error() + } else { + body.Msg = "OK" + body.Data = resp + } + httpx.OkJson(w, body) +} +``` + +## Modify the `handler` template +```shell +$ vim ~/.goctl/api/handler.tpl +``` + +Replace the template with the following +```go +package handler + +import ( + "net/http" + "greet/response"// ① + + {{.ImportPackages}} +) + +func {{.HandlerName}}(ctx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + {{if .HasRequest}}var req types.{{.RequestType}} + if err := httpx.Parse(r, &req); err != nil { + httpx.Error(w, err) + return + }{{end}} + + l := logic.New{{.LogicType}}(r.Context(), ctx) + {{if .HasResp}}resp, {{end}}err := l.{{.Call}}({{if .HasRequest}}req{{end}}) + {{if .HasResp}}reponse.Response(w, resp, err){{else}}reponse.Response(w, nil, err){{end}}//② + + } +} +``` + +① Replace with your real `response` package name, for reference only + +② Customized template content + +> [!TIP] +> +> 1.If there is no local `~/.goctl/api/handler.tpl` file, you can initialize it with the template initialization command `goctl template init` + +## Comparison +* Before +```go +func GreetHandler(ctx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.Request + if err := httpx.Parse(r, &req); err != nil { + httpx.Error(w, err) + return + } + + l := logic.NewGreetLogic(r.Context(), ctx) + resp, err := l.Greet(req) + // The following content will be replaced by custom templates + if err != nil { + httpx.Error(w, err) + } else { + httpx.OkJson(w, resp) + } + } +} +``` +* After +```go +func GreetHandler(ctx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.Request + if err := httpx.Parse(r, &req); err != nil { + httpx.Error(w, err) + return + } + + l := logic.NewGreetLogic(r.Context(), ctx) + resp, err := l.Greet(req) + reponse.Response(w, resp, err) + } +} +``` + +## Comparison of response body + +* Before +```json +{ + "message": "Hello go-zero!" +} +``` + +* After +```json +{ + "code": 0, + "msg": "OK", + "data": { + "message": "Hello go-zero!" + } +} +``` + +# Summary +This document only describes the process of customizing the template for the corresponding example of http, in addition to the following scenarios of customizing the template. +* model layer adds kmq +* model layer to generate the model instance of the option to be valid +* http customize the corresponding format + ... \ No newline at end of file diff --git a/go-zero.dev/en/timing-wheel.md b/go-zero.dev/en/timing-wheel.md new file mode 100644 index 00000000..6b4ec3b3 --- /dev/null +++ b/go-zero.dev/en/timing-wheel.md @@ -0,0 +1,321 @@ +# TimingWheel +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +This article introduces the **delayed operation** in `go-zero`. **Delayed operation**, two options can be used: + + +1. `Timer`: The timer maintains a priority queue, executes it at the time, and then stores the tasks that need to be executed in the map +2. The `timingWheel` in `collection` maintains an array for storing task groups, and each slot maintains a doubly linked list of tasks. When the execution starts, the timer executes the tasks in a slot every specified time. + + + +Scheme 2 reduces the maintenance task from the `priority queue O(nlog(n))` to the `doubly linked list O(1)`, and the execution of the task only needs to poll the tasks `O(N)` at a time point. Priority queue, put and delete elements `O(nlog(n))`. + + +## timingWheel in cache + + +First, let's first talk about the use of TimingWheel in the cache of collection: + + +```go +timingWheel, err := NewTimingWheel(time.Second, slots, func(k, v interface{}) { + key, ok := k.(string) + if !ok { + return + } + cache.Del(key) +}) +if err != nil { + return nil, err +} + +cache.timingWheel = timingWheel +``` + + +This is the initialization of `cache` and the initialization of `timingWheel` at the same time for key expiration processing. The parameters in turn represent: + + +- `interval`: Time division scale +- `numSlots`: time slots +- `execute`: execute a function at a point in time + + + +The function executed in the `cache` is to **delete the expired key**, and this expiration is controlled by the `timingWheel` to advance the time. + + +**Next, let's learn about it through the use of timingWheel by cache. ** + + +### Initialization + + +```go +func newTimingWheelWithClock(interval time.Duration, numSlots int, execute Execute, ticker timex.Ticker) ( + *TimingWheel, error) { + tw := &TimingWheel{ + interval: interval, // Single time grid time interval + ticker: ticker, // Timer, do time push, advance by interval + slots: make([]*list.List, numSlots), // Time wheel + timers: NewSafeMap(), // Store the map of task{key, value} [parameters needed to execute execute] + tickedPos: numSlots - 1, // at previous virtual circle + execute: execute, // Execution function + numSlots: numSlots, // Initialize slots num + setChannel: make(chan timingEntry), // The following channels are used for task delivery + moveChannel: make(chan baseEntry), + removeChannel: make(chan interface{}), + drainChannel: make(chan func(key, value interface{})), + stopChannel: make(chan lang.PlaceholderType), + } + // Prepare all the lists stored in the slot + tw.initSlots() + // Open asynchronous coroutine, use channel for task communication and delivery + go tw.run() + + return tw, nil +} +``` + + +![76108cc071154e2faa66eada81857fb0~tplv-k3u1fbpfcp-zoom-1.image.png](./resource/76108cc071154e2faa66eada81857fb0_tplv-k3u1fbpfcp-zoom-1.image.png) + + +The above is a more intuitive display of the **"time wheel"** of the `timingWheel`, and the details of the advancement will be explained later around this picture. + + +`go tw.run()` opens a coroutine for time promotion: + + +```go +func (tw *TimingWheel) run() { + for { + select { + // Timer do time push -> scanAndRunTasks() + case <-tw.ticker.Chan(): + tw.onTick() + // add task will enter task into setChannel + case task := <-tw.setChannel: + tw.setTask(&task) + ... + } + } +} +``` + + +It can be seen that the `timer` execution is started at the time of initialization, and it is rotated in the `internal` time period, and then the bottom layer keeps getting the tasks from the `list` in the `slot` and handing them over to the `execute` for execution. + + +![3bbddc1ebb79455da91dfcf3da6bc72f~tplv-k3u1fbpfcp-zoom-1.image.png](./resource/3bbddc1ebb79455da91dfcf3da6bc72f_tplv-k3u1fbpfcp-zoom-1.image.png) + + +### Task Operation + + +The next step is to set the `cache key`: + + +```go +func (c *Cache) Set(key string, value interface{}) { + c.lock.Lock() + _, ok := c.data[key] + c.data[key] = value + c.lruCache.add(key) + c.lock.Unlock() + + expiry := c.unstableExpiry.AroundDuration(c.expire) + if ok { + c.timingWheel.MoveTimer(key, expiry) + } else { + c.timingWheel.SetTimer(key, value, expiry) + } +} +``` + + +1. First look at whether this key exists in the `data map` +1. If it exists, update `expire` -> `MoveTimer()` +1. Set the key for the first time -> `SetTimer()` + + + +So the use of `timingWheel` is clear. Developers can `add` or `update` according to their needs. + + +At the same time, when we follow the source code, we will find that: `SetTimer() MoveTimer()` all transports tasks to channel, and the coroutine opened in `run()` continuously takes out the task operations of `channel`. + + +> `SetTimer() -> setTask()`: +> - not exist task:`getPostion -> pushBack to list -> setPosition` +> - exist task:`get from timers -> moveTask()` +> +`MoveTimer() -> moveTask()` + + + +From the above call chain, there is a function that will be called: `moveTask()` + + +```go +func (tw *TimingWheel) moveTask(task baseEntry) { + // timers: Map => Get [positionEntry「pos, task」] by key + val, ok := tw.timers.Get(task.key) + if !ok { + return + } + + timer := val.(*positionEntry) + // {delay The delay time is less than a time grid interval, and there is no smaller scale, indicating that the task should be executed immediately + if task.delay < tw.interval { + threading.GoSafe(func() { + tw.execute(timer.item.key, timer.item.value) + }) + return + } + // If> interval, the new pos, circle in the time wheel is calculated by the delay time delay + pos, circle := tw.getPositionAndCircle(task.delay) + if pos >= timer.pos { + timer.item.circle = circle + // Move offset before and after recording. To re-enter the team for later process + timer.item.diff = pos - timer.pos + } else if circle > 0 { + // Move to the next layer and convert circle to part of diff + circle-- + timer.item.circle = circle + // Because it is an array, add numSlots [that is equivalent to going to the next level] + timer.item.diff = tw.numSlots + pos - timer.pos + } else { + // If the offset is advanced, the task is still in the first layer at this time + // Mark to delete the old task, and re-enter the team, waiting to be executed + timer.item.removed = true + newItem := &timingEntry{ + baseEntry: task, + value: timer.item.value, + } + tw.slots[pos].PushBack(newItem) + tw.setTimerPosition(pos, newItem) + } +} +``` + + +The above process has the following situations: + + +- `delay = old`:`` + - `newCircle> 0`: Calculate diff and convert circle to the next layer, so diff + numslots + - If only the delay time is shortened, delete the old task mark, re-add it to the list, and wait for the next loop to be executed + + + +### Execute + + +In the previous initialization, the timer in `run()` kept advancing, and the process of advancing was mainly to pass the tasks in the list to the executed `execute func`. Let's start with the execution of the timer: + + +```go +// Timer "It will be executed every internal" +func (tw *TimingWheel) onTick() { + // Update the current execution tick position every time it is executed + tw.tickedPos = (tw.tickedPos + 1) % tw.numSlots + // Get the doubly linked list of stored tasks in the tick position at this time + l := tw.slots[tw.tickedPos] + tw.scanAndRunTasks(l) +} +``` + + +Next is how to execute `execute`: + + +```go +func (tw *TimingWheel) scanAndRunTasks(l *list.List) { + // Store the task{key, value} that needs to be executed at present [parameters required by execute, which are passed to execute in turn] + var tasks []timingTask + + for e := l.Front(); e != nil; { + task := e.Value.(*timingEntry) + // Mark the deletion, do the real deletion in scan "Delete the map data" + if task.removed { + next := e.Next() + l.Remove(e) + tw.timers.Del(task.key) + e = next + continue + } else if task.circle > 0 { + // The current execution point has expired, but it is not at the first level at the same time, so now that the current level has been completed, it will drop to the next level + // But did not modify pos + task.circle-- + e = e.Next() + continue + } else if task.diff > 0 { + // Because the diff has been marked before, you need to enter the queue again + next := e.Next() + l.Remove(e) + pos := (tw.tickedPos + task.diff) % tw.numSlots + tw.slots[pos].PushBack(task) + tw.setTimerPosition(pos, task) + task.diff = 0 + e = next + continue + } + // The above cases are all cases that cannot be executed, and those that can be executed will be added to tasks + tasks = append(tasks, timingTask{ + key: task.key, + value: task.value, + }) + next := e.Next() + l.Remove(e) + tw.timers.Del(task.key) + e = next + } + // for range tasks, and then execute each task->execute + tw.runTasks(tasks) +} +``` + + +The specific branching situation is explained in the comments. When you look at it, it can be combined with the previous `moveTask()`, where the `circle` drops, and the calculation of `diff` is the key to linking the two functions. + + +As for the calculation of `diff`, the calculation of `pos, circle` is involved: + + +```go +// interval: 4min, d: 60min, numSlots: 16, tickedPos = 15 +// step = 15, pos = 14, circle = 0 +func (tw *TimingWheel) getPositionAndCircle(d time.Duration) (pos int, circle int) { + steps := int(d / tw.interval) + pos = (tw.tickedPos + steps) % tw.numSlots + circle = (steps - 1) / tw.numSlots + return +} +``` + + +The above process can be simplified to the following: + +```go +steps = d / interval +pos = step % numSlots - 1 +circle = (step - 1) / numSlots +``` + + + +## Summary + + +The `timingWheel` is driven by the timer. As the time advances, the tasks of the `list` "doubly linked list" in the **current time grid** will be taken out and passed to the `execute` for execution. + + +In terms of time separation, the time wheel has `circle` layers, so that the original `numSlots` can be reused continuously, because the timer is constantly `loop`, and execution can drop the upper layer `slot` to the lower layer. You can execute the task up to the upper level continuously in the `loop`. + + +There are many useful component tools in `go-zero`. Good use of tools is of great help to improve service performance and development efficiency. I hope this article can bring you some gains. diff --git a/go-zero.dev/en/tokenlimit.md b/go-zero.dev/en/tokenlimit.md new file mode 100644 index 00000000..71e52086 --- /dev/null +++ b/go-zero.dev/en/tokenlimit.md @@ -0,0 +1,155 @@ +# tokenlimit +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +This section will introduce its basic usage through token limit (tokenlimit). + +## Usage + +```go +const ( + burst = 100 + rate = 100 + seconds = 5 +) + +store := redis.NewRedis("localhost:6379", "node", "") +fmt.Println(store.Ping()) +// New tokenLimiter +limiter := limit.NewTokenLimiter(rate, burst, store, "rate-test") +timer := time.NewTimer(time.Second * seconds) +quit := make(chan struct{}) +defer timer.Stop() +go func() { + <-timer.C + close(quit) +}() + +var allowed, denied int32 +var wait sync.WaitGroup +for i := 0; i < runtime.NumCPU(); i++ { + wait.Add(1) + go func() { + for { + select { + case <-quit: + wait.Done() + return + default: + if limiter.Allow() { + atomic.AddInt32(&allowed, 1) + } else { + atomic.AddInt32(&denied, 1) + } + } + } + }() +} + +wait.Wait() +fmt.Printf("allowed: %d, denied: %d, qps: %d\n", allowed, denied, (allowed+denied)/seconds) +``` + + +## tokenlimit + +On the whole, the token bucket production logic is as follows: +- The average sending rate configured by the user is r, then a token is added to the bucket every 1/r second; +- Assume that at most b tokens can be stored in the bucket. If the token bucket is full when the token arrives, then the token will be discarded; +- When the traffic enters at the rate v, the token is taken from the bucket at the rate v, the traffic that gets the token passes, and the traffic that does not get the token does not pass, and the fuse logic is executed; + + + +`go-zero` adopts the method of `lua script` under both types of current limiters, relying on redis to achieve distributed current limiting, and `lua script` can also achieve atomicity of token production and read operations. + +Let's take a look at several key attributes controlled by `lua script`: + +| argument | mean | +| --- | --- | +| ARGV[1] | rate 「How many tokens are generated per second」 | +| ARGV[2] | burst 「Maximum token bucket」 | +| ARGV[3] | now_time「Current timestamp」 | +| ARGV[4] | get token nums 「The number of tokens that the developer needs to obtain」 | +| KEYS[1] | Tokenkey representing the resource | +| KEYS[2] | The key that represents the refresh time | + + + +```lua +-- Return whether the expected token can be obtained alive + +local rate = tonumber(ARGV[1]) +local capacity = tonumber(ARGV[2]) +local now = tonumber(ARGV[3]) +local requested = tonumber(ARGV[4]) + +-- fill_time:How long does it take to fill the token_bucket +local fill_time = capacity/rate +-- Round down the fill time +local ttl = math.floor(fill_time*2) + +-- Get the number of remaining tokens in the current token_bucket +-- If it is the first time to enter, set the number of token_bucket to the maximum value of the token bucket +local last_tokens = tonumber(redis.call("get", KEYS[1])) +if last_tokens == nil then + last_tokens = capacity +end + +-- The time when the token_bucket was last updated +local last_refreshed = tonumber(redis.call("get", KEYS[2])) +if last_refreshed == nil then + last_refreshed = 0 +end + +local delta = math.max(0, now-last_refreshed) +-- Calculate the number of new tokens based on the span between the current time and the last update time, and the rate of token production +-- If it exceeds max_burst, excess tokens produced will be discarded +local filled_tokens = math.min(capacity, last_tokens+(delta*rate)) +local allowed = filled_tokens >= requested +local new_tokens = filled_tokens +if allowed then + new_tokens = filled_tokens - requested +end + +-- Update the new token number and update time +redis.call("setex", KEYS[1], ttl, new_tokens) +redis.call("setex", KEYS[2], ttl, now) + +return allowed +``` + + +It can be seen from the above that the `lua script`: only involves the operation of the token, ensuring that the token is produced and read reasonably. + + +## Function analysis + + +![](https://cdn.nlark.com/yuque/0/2020/png/261626/1606107337223-7756ecdf-acb6-48c2-9ff5-959de01a1a03.png#align=left&display=inline&height=896&margin=%5Bobject%20Object%5D&originHeight=896&originWidth=2038&status=done&style=none&width=2038) + + +Seen from the above flow: + + +1. There are multiple guarantee mechanisms to ensure that the current limit will be completed. +1. If the `redis limiter` fails, at least in the process `rate limiter` will cover it. +1. Retry the `redis limiter` mechanism to ensure that it runs as normally as possible. + + + +## Summary + + +The `tokenlimit` current limiting scheme in `go-zero` is suitable for instantaneous traffic shocks, and the actual request scenario is not at a constant rate. The token bucket is quite pre-request, and when the real request arrives, it won't be destroyed instantly. When the traffic hits a certain level, consumption will be carried out at a predetermined rate. + + +However, in the production of `token`, dynamic adjustment cannot be made according to the current flow situation, and it is not flexible enough, and further optimization can be carried out. In addition, you can refer to [Token bucket WIKI](https://en.wikipedia.org/wiki/Token_bucket) which mentioned hierarchical token buckets, which are divided into different queues according to different traffic bandwidths. + + +## Reference + +- [go-zero tokenlimit](https://github.com/zeromicro/go-zero/blob/master/core/limit/tokenlimit.go) +- [Redis Rate](https://github.com/go-redis/redis_rate) + + + diff --git a/go-zero.dev/en/tool-center.md b/go-zero.dev/en/tool-center.md new file mode 100644 index 00000000..20cf6d26 --- /dev/null +++ b/go-zero.dev/en/tool-center.md @@ -0,0 +1,8 @@ +# Tools +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +In go-zero, a lot of tools to improve engineering efficiency are provided, such as api and rpc generation. On this basis, the compilation of api files seems so weak. +Because of the lack of highlighting, code hints, template generation, etc., this section will show you how go-zero solves these problems. This section contains the following subsections: +* [Intellij plugin](intellij.md) +* [VSCode plugin](vscode.md) \ No newline at end of file diff --git a/go-zero.dev/en/trace.md b/go-zero.dev/en/trace.md new file mode 100644 index 00000000..ea7a79f0 --- /dev/null +++ b/go-zero.dev/en/trace.md @@ -0,0 +1,188 @@ +# Trace +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + + +## Foreword + +In the microservice architecture, the call chain may be very long, from `http` to `rpc`, and from `rpc` to `http`. Developers want to know the call status and performance of each link, the best solution is **full link tracking**. + +The tracking method is to generate its own `spanID` at the beginning of a request, and pass it down along the entire request link. We use this `spanID` to view the status of the entire link and performance issues. + +Let's take a look at the link implementation of `go-zero`. + +## Code structure + +- [spancontext](https://github.com/zeromicro/go-zero/blob/master/core/trace/spancontext.go) :保存链路的上下文信息「traceid,spanid,或者是其他想要传递的内容」 +- [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` 实现 + +![](https://static.gocn.vip/photo/2020/2f244477-4ed3-4ad1-8003-ff82cbe2f8a0.png?x-oss-process=image/resize,w_1920) + +## Concept + +### SpanContext + +Before introducing `span`, first introduce `context`. SpanContext saves the context information of distributed tracing, including Trace id, Span id and other content that needs to be passed downstream. The implementation of OpenTracing needs to pass the SpanContext through a certain protocol to associate the Span in different processes to the same Trace. For HTTP requests, SpanContext is generally passed using HTTP headers. + +Below is the `spanContext` implemented by `go-zero` by default + +```go +type spanContext struct { + traceId string // TraceID represents the globally unique ID of tracer + spanId string // SpanId indicates the unique ID of a span in a single trace, which is unique in the trace +} +``` + +At the same time, developers can also implement the interface methods provided by `SpanContext` to realize their own contextual information transfer: + +```go +type SpanContext interface { + TraceId() string // get TraceId + SpanId() string // get SpanId + Visit(fn func(key, val string) bool) // Custom operation TraceId, SpanId +} +``` + +### Span + +A REST call or database operation, etc., can be used as a `span`. `span` is the smallest tracking unit of distributed tracing. A trace is composed of multiple spans. The tracking information includes the following information: + +```go +type Span struct { + ctx spanContext + serviceName string + operationName string + startTime time.Time + flag string + children int +} +``` + +Judging from the definition structure of `span`: In microservices, this is a complete sub-calling process, with the start of the call `startTime`, the context structure `spanContext` that marks its own unique attribute, and the number of child nodes of fork. + +## Example application + +In `go-zero`, http and rpc have been integrated as built-in middleware. We use [http](https://github.com/zeromicro/go-zero/blob/master/rest/handler/tracinghandler.go), [rpc](https://github.com/tal-tech /go-zero/blob/master/zrpc/internal/clientinterceptors/tracinginterceptor.go), take a look at how `tracing` is used: + +### HTTP + +```go +func TracingHandler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // **1** + carrier, err := trace.Extract(trace.HttpFormat, r.Header) + // ErrInvalidCarrier means no trace id was set in http header + if err != nil && err != trace.ErrInvalidCarrier { + logx.Error(err) + } + + // **2** + ctx, span := trace.StartServerSpan(r.Context(), carrier, sysx.Hostname(), r.RequestURI) + defer span.Finish() + // **5** + r = r.WithContext(ctx) + + next.ServeHTTP(w, r) + }) +} + +func StartServerSpan(ctx context.Context, carrier Carrier, serviceName, operationName string) ( + context.Context, tracespec.Trace) { + span := newServerSpan(carrier, serviceName, operationName) + // **4** + return context.WithValue(ctx, tracespec.TracingKey, span), span +} + +func newServerSpan(carrier Carrier, serviceName, operationName string) tracespec.Trace { + // **3** + traceId := stringx.TakeWithPriority(func() string { + if carrier != nil { + return carrier.Get(traceIdKey) + } + return "" + }, func() string { + return stringx.RandId() + }) + spanId := stringx.TakeWithPriority(func() string { + if carrier != nil { + return carrier.Get(spanIdKey) + } + return "" + }, func() string { + return initSpanId + }) + + return &Span{ + ctx: spanContext{ + traceId: traceId, + spanId: spanId, + }, + serviceName: serviceName, + operationName: operationName, + startTime: timex.Time(), + // 标记为server + flag: serverFlag, + } +} +``` + +1. Set header -> carrier to get the traceId and other information in the header +1. Open a new span and encapsulate **"traceId, spanId"** in the context +1. Obtain traceId and spanId from the aforementioned carrier "that is, header" + -See if it is set in the header + -If it is not set, it will be randomly generated and returned +1. Generate a new ctx from `request`, encapsulate the corresponding information in ctx, and return +1. From the above context, copy a copy to the current `request` + +![](https://static.gocn.vip/photo/2020/a30daba2-ad12-477c-8ce5-131ef1cc3e76.png?x-oss-process=image/resize,w_1920) + +In this way, the information of the `span` is passed to the downstream service along with the `request`. + +### RPC + +There are `client, server` in rpc, so from `tracing` there are also `clientTracing, serverTracing`. The logic of `serveTracing` is basically the same as that of http. Let’s take a look at how `clientTracing` is used? + +```go +func TracingInterceptor(ctx context.Context, method string, req, reply interface{}, + cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { + // open clientSpan + ctx, span := trace.StartClientSpan(ctx, cc.Target(), method) + defer span.Finish() + + var pairs []string + span.Visit(func(key, val string) bool { + pairs = append(pairs, key, val) + return true + }) + // **3** Add the data in the pair to ctx in the form of a map + ctx = metadata.AppendToOutgoingContext(ctx, pairs...) + + return invoker(ctx, method, req, reply, cc, opts...) +} + +func StartClientSpan(ctx context.Context, serviceName, operationName string) (context.Context, tracespec.Trace) { + // **1** + if span, ok := ctx.Value(tracespec.TracingKey).(*Span); ok { + // **2** + return span.Fork(ctx, serviceName, operationName) + } + + return ctx, emptyNoopSpan +} +``` + +1. Get the span context information brought down by the upstream +1. Create a new ctx from the acquired span, span "inherit the traceId of the parent span" +1. Add the span generated data to ctx, pass it to the next middleware, and flow downstream + +## Summary + +`go-zero` obtains the link traceID by intercepting the request, and then assigns a root Span at the entry of the middleware function, and then splits the child Spans in subsequent operations. Each span has its own specific identification. After Finsh Will be collected in the link tracking system. Developers can trace the traceID through the ELK tool to see the entire call chain. + +At the same time, `go-zero` does not provide a complete set of `trace` link solutions. Developers can encapsulate the existing `span` structure of `go-zero`, build their own reporting system, and access links such as `jaeger, zipkin`, etc. Tracking tool. + +## Reference + +- [go-zero trace](https://github.com/zeromicro/go-zero/tree/master/core/trace) \ No newline at end of file diff --git a/go-zero.dev/en/vscode.md b/go-zero.dev/en/vscode.md new file mode 100644 index 00000000..e04ec329 --- /dev/null +++ b/go-zero.dev/en/vscode.md @@ -0,0 +1,42 @@ +# vs code plugin +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + +The plug-in can be installed on the 1.46.0+ version of Visual Studio Code. First, please make sure that your Visual Studio Code version meets the requirements and the goctl command line tool has been installed. If Visual Studio Code is not installed, please install and open Visual Studio Code. Navigate to the "Extensions" pane, search for goctl and install this extension (publisher ID is "xiaoxin-technology.goctl"). + +For the extension of Visual Studio Code, please refer to [here](https://code.visualstudio.com/docs/editor/extension-gallery). + +## Features + +* Syntax highlighting +* Jump to definition/reference +* Code formatting +* Code block hint + +### Syntax highlighting + +### Jump to definition/reference + +![jump](./resource/jump.gif) + +### Code formatting + +Invoke the goctl command line formatting tool, please make sure that goctl has been added to `$PATH` and has executable permissions before use + +### Code block hint + +#### info block + +![info](./resource/info.gif) + +#### type block + +![type](./resource/type.gif) + +#### service block + +![type](./resource/service.gif) + +#### handler block + +![type](./resource/handler.gif) diff --git a/go-zero.dev/en/wechat.md b/go-zero.dev/en/wechat.md new file mode 100644 index 00000000..45cbcfc0 --- /dev/null +++ b/go-zero.dev/en/wechat.md @@ -0,0 +1,31 @@ +# Wechat +> [!TIP] +> This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please [PR](doc-contibute.md) + + +Microservices actual combat is the official official account of go-zero, where the latest go-zero best practices will be released, synchronized go night reading, go open source, GopherChina, Tencent Cloud Developer Conference and other channels about the latest go-zero Technology and information. + + + + + + + + + + + + +
NameAuthorQrCode
微服务实战kevwan微服务实践
+ +# Articles(Simplified Chinese) +Here are some articles. If you want to get more go-zero best practice dry goods, you can follow the public account for the latest developments. +* [《一文读懂云原生 go-zero 微服务框架》](https://mp.weixin.qq.com/s/gszj3-fwfcof5Tt2Th4dFA) +* [《你还在手撕微服务?快试试 go-zero 的微服务自动生成》](https://mp.weixin.qq.com/s/Qvi-g3obgD_FVJ7CK3O56w) +* [《最简单的Go Dockerfile编写姿势,没有之一!》](https://mp.weixin.qq.com/s/VLBiIbZStKhb7uth1ndgQQ) +* [《通过MapReduce降低服务响应时间》](https://mp.weixin.qq.com/s/yxXAIK1eC_X22DH4ssZSag) +* [《微服务过载保护原理与实战](https://mp.weixin.qq.com/s/CWzf6CY2R12Xd-rIYVvdPQ) +* [《最简单的 K8S 部署文件编写姿势,没有之一!》](https://mp.weixin.qq.com/s/1GOMxlI8ocOL3U_I2TKPzQ) +* [《go-zero 如何应对海量定时/延迟任务?》](https://mp.weixin.qq.com/s/CiZ5SpuT-VN8V9wil8_iGg) +* [《go-zero 如何扛住流量冲击(一)》](https://mp.weixin.qq.com/s/xnJIm3asMncBfbtXo22sZw) +* [《服务自适应降载保护设计》](https://mp.weixin.qq.com/s/cgjCL59e3CDWhsxzwkuKBg) \ No newline at end of file diff --git a/go-zero.dev/javascript/gitalk.js b/go-zero.dev/javascript/gitalk.js new file mode 100644 index 00000000..af666bb8 --- /dev/null +++ b/go-zero.dev/javascript/gitalk.js @@ -0,0 +1,5886 @@ +!function (e, t) { + "object" == typeof exports && "object" == typeof module ? module.exports = t() : "function" == typeof define && define.amd ? define([], t) : "object" == typeof exports ? exports.Gitalk = t() : e.Gitalk = t() +}(this, function () { + return function (e) { + function t(r) { + if (n[r]) return n[r].exports; + var o = n[r] = {i: r, l: !1, exports: {}}; + return e[r].call(o.exports, o, o.exports, t), o.l = !0, o.exports + } + + var n = {}; + return t.m = e, t.c = n, t.d = function (e, n, r) { + t.o(e, n) || Object.defineProperty(e, n, {configurable: !1, enumerable: !0, get: r}) + }, t.n = function (e) { + var n = e && e.__esModule ? function () { + return e.default + } : function () { + return e + }; + return t.d(n, "a", n), n + }, t.o = function (e, t) { + return Object.prototype.hasOwnProperty.call(e, t) + }, t.p = "/dist", t(t.s = 82) + }([function (e, t) { + var n = e.exports = {version: "2.6.11"}; + "number" == typeof __e && (__e = n) + }, function (e, t) { + var n = e.exports = "undefined" != typeof window && window.Math == Math ? window : "undefined" != typeof self && self.Math == Math ? self : Function("return this")(); + "number" == typeof __g && (__g = n) + }, function (e, t, n) { + var r = n(39)("wks"), o = n(25), i = n(1).Symbol, a = "function" == typeof i; + (e.exports = function (e) { + return r[e] || (r[e] = a && i[e] || (a ? i : o)("Symbol." + e)) + }).store = r + }, function (e, t, n) { + "use strict"; + + function r(e) { + return "[object Array]" === C.call(e) + } + + function o(e) { + return void 0 === e + } + + function i(e) { + return null !== e && !o(e) && null !== e.constructor && !o(e.constructor) && "function" == typeof e.constructor.isBuffer && e.constructor.isBuffer(e) + } + + function a(e) { + return "[object ArrayBuffer]" === C.call(e) + } + + function u(e) { + return "undefined" != typeof FormData && e instanceof FormData + } + + function s(e) { + return "undefined" != typeof ArrayBuffer && ArrayBuffer.isView ? ArrayBuffer.isView(e) : e && e.buffer && e.buffer instanceof ArrayBuffer + } + + function c(e) { + return "string" == typeof e + } + + function l(e) { + return "number" == typeof e + } + + function f(e) { + return null !== e && "object" == typeof e + } + + function p(e) { + return "[object Date]" === C.call(e) + } + + function d(e) { + return "[object File]" === C.call(e) + } + + function h(e) { + return "[object Blob]" === C.call(e) + } + + function m(e) { + return "[object Function]" === C.call(e) + } + + function v(e) { + return f(e) && m(e.pipe) + } + + function y(e) { + return "undefined" != typeof URLSearchParams && e instanceof URLSearchParams + } + + function g(e) { + return e.replace(/^\s*/, "").replace(/\s*$/, "") + } + + function b() { + return ("undefined" == typeof navigator || "ReactNative" !== navigator.product && "NativeScript" !== navigator.product && "NS" !== navigator.product) && ("undefined" != typeof window && "undefined" != typeof document) + } + + function w(e, t) { + if (null !== e && void 0 !== e) if ("object" != typeof e && (e = [e]), r(e)) for (var n = 0, o = e.length; n < o; n++) t.call(null, e[n], n, e); else for (var i in e) Object.prototype.hasOwnProperty.call(e, i) && t.call(null, e[i], i, e) + } + + function _() { + function e(e, n) { + "object" == typeof t[n] && "object" == typeof e ? t[n] = _(t[n], e) : t[n] = e + } + + for (var t = {}, n = 0, r = arguments.length; n < r; n++) w(arguments[n], e); + return t + } + + function x() { + function e(e, n) { + "object" == typeof t[n] && "object" == typeof e ? t[n] = x(t[n], e) : t[n] = "object" == typeof e ? x({}, e) : e + } + + for (var t = {}, n = 0, r = arguments.length; n < r; n++) w(arguments[n], e); + return t + } + + function S(e, t, n) { + return w(t, function (t, r) { + e[r] = n && "function" == typeof t ? E(t, n) : t + }), e + } + + var E = n(72), C = Object.prototype.toString; + e.exports = { + isArray: r, + isArrayBuffer: a, + isBuffer: i, + isFormData: u, + isArrayBufferView: s, + isString: c, + isNumber: l, + isObject: f, + isUndefined: o, + isDate: p, + isFile: d, + isBlob: h, + isFunction: m, + isStream: v, + isURLSearchParams: y, + isStandardBrowserEnv: b, + forEach: w, + merge: _, + deepMerge: x, + extend: S, + trim: g + } + }, function (e, t, n) { + var r = n(1), o = n(0), i = n(13), a = n(11), u = n(12), s = function (e, t, n) { + var c, l, f, p = e & s.F, d = e & s.G, h = e & s.S, m = e & s.P, v = e & s.B, y = e & s.W, + g = d ? o : o[t] || (o[t] = {}), b = g.prototype, w = d ? r : h ? r[t] : (r[t] || {}).prototype; + d && (n = t); + for (c in n) (l = !p && w && void 0 !== w[c]) && u(g, c) || (f = l ? w[c] : n[c], g[c] = d && "function" != typeof w[c] ? n[c] : v && l ? i(f, r) : y && w[c] == f ? function (e) { + var t = function (t, n, r) { + if (this instanceof e) { + switch (arguments.length) { + case 0: + return new e; + case 1: + return new e(t); + case 2: + return new e(t, n) + } + return new e(t, n, r) + } + return e.apply(this, arguments) + }; + return t.prototype = e.prototype, t + }(f) : m && "function" == typeof f ? i(Function.call, f) : f, m && ((g.virtual || (g.virtual = {}))[c] = f, e & s.R && b && !b[c] && a(b, c, f))) + }; + s.F = 1, s.G = 2, s.S = 4, s.P = 8, s.B = 16, s.W = 32, s.U = 64, s.R = 128, e.exports = s + }, function (e, t, n) { + "use strict"; + Object.defineProperty(t, "__esModule", {value: !0}), function (e) { + function r() { + return null + } + + function o(e) { + var t = e.nodeName, n = e.attributes; + e.attributes = {}, t.defaultProps && _(e.attributes, t.defaultProps), n && _(e.attributes, n) + } + + function i(e, t) { + var n, r, o; + if (t) { + for (o in t) if (n = W.test(o)) break; + if (n) { + r = e.attributes = {}; + for (o in t) t.hasOwnProperty(o) && (r[W.test(o) ? o.replace(/([A-Z0-9])/, "-$1").toLowerCase() : o] = t[o]) + } + } + } + + function a(e, t, n) { + var r = t && t._preactCompatRendered && t._preactCompatRendered.base; + r && r.parentNode !== t && (r = null), r || (r = t.children[0]); + for (var o = t.childNodes.length; o--;) t.childNodes[o] !== r && t.removeChild(t.childNodes[o]); + var i = R.render(e, t, r); + return t && (t._preactCompatRendered = i && (i._component || {base: i})), "function" == typeof n && n(), i && i._component || i + } + + function u(e, t, n, r) { + var o = R.h(J, {context: e.context}, t), i = a(o, n); + return r && r(i), i._component || i.base + } + + function s(e) { + var t = e._preactCompatRendered && e._preactCompatRendered.base; + return !(!t || t.parentNode !== e) && (R.render(R.h(r), e, t), !0) + } + + function c(e) { + return h.bind(null, e) + } + + function l(e, t) { + for (var n = t || 0; n < e.length; n++) { + var r = e[n]; + Array.isArray(r) ? l(r) : r && "object" == typeof r && !y(r) && (r.props && r.type || r.attributes && r.nodeName || r.children) && (e[n] = h(r.type || r.nodeName, r.props || r.attributes, r.children)) + } + } + + function f(e) { + return "function" == typeof e && !(e.prototype && e.prototype.render) + } + + function p(e) { + return C({ + displayName: e.displayName || e.name, render: function () { + return e(this.props, this.context) + } + }) + } + + function d(e) { + var t = e[$]; + return t ? !0 === t ? e : t : (t = p(e), Object.defineProperty(t, $, { + configurable: !0, + value: !0 + }), t.displayName = e.displayName, t.propTypes = e.propTypes, t.defaultProps = e.defaultProps, Object.defineProperty(e, $, { + configurable: !0, + value: t + }), t) + } + + function h() { + for (var e = [], t = arguments.length; t--;) e[t] = arguments[t]; + return l(e, 2), m(R.h.apply(void 0, e)) + } + + function m(e) { + e.preactCompatNormalized = !0, w(e), f(e.nodeName) && (e.nodeName = d(e.nodeName)); + var t = e.attributes.ref, n = t && typeof t; + return !Z || "string" !== n && "number" !== n || (e.attributes.ref = g(t, Z)), b(e), e + } + + function v(e, t) { + for (var n = [], r = arguments.length - 2; r-- > 0;) n[r] = arguments[r + 2]; + if (!y(e)) return e; + var o = e.attributes || e.props, i = R.h(e.nodeName || e.type, o, e.children || o && o.children), + a = [i, t]; + return n && n.length ? a.push(n) : t && t.children && a.push(t.children), m(R.cloneElement.apply(void 0, a)) + } + + function y(e) { + return e && (e instanceof Y || e.$$typeof === H) + } + + function g(e, t) { + return t._refProxies[e] || (t._refProxies[e] = function (n) { + t && t.refs && (t.refs[e] = n, null === n && (delete t._refProxies[e], t = null)) + }) + } + + function b(e) { + var t = e.nodeName, n = e.attributes; + if (n && "string" == typeof t) { + var r = {}; + for (var o in n) r[o.toLowerCase()] = o; + if (r.ondoubleclick && (n.ondblclick = n[r.ondoubleclick], delete n[r.ondoubleclick]), r.onchange && ("textarea" === t || "input" === t.toLowerCase() && !/^fil|che|rad/i.test(n.type))) { + var i = r.oninput || "oninput"; + n[i] || (n[i] = P([n[i], n[r.onchange]]), delete n[r.onchange]) + } + } + } + + function w(e) { + var t = e.attributes; + if (t) { + var n = t.className || t.class; + n && (t.className = n) + } + } + + function _(e, t) { + for (var n in t) t.hasOwnProperty(n) && (e[n] = t[n]); + return e + } + + function x(e, t) { + for (var n in e) if (!(n in t)) return !0; + for (var r in t) if (e[r] !== t[r]) return !0; + return !1 + } + + function S(e) { + return e && e.base || e + } + + function E() { + } + + function C(e) { + function t(e, t) { + O(this), I.call(this, e, t, X), T.call(this, e, t) + } + + return e = _({constructor: t}, e), e.mixins && N(e, k(e.mixins)), e.statics && _(t, e.statics), e.propTypes && (t.propTypes = e.propTypes), e.defaultProps && (t.defaultProps = e.defaultProps), e.getDefaultProps && (t.defaultProps = e.getDefaultProps()), E.prototype = I.prototype, t.prototype = _(new E, e), t.displayName = e.displayName || "Component", t + } + + function k(e) { + for (var t = {}, n = 0; n < e.length; n++) { + var r = e[n]; + for (var o in r) r.hasOwnProperty(o) && "function" == typeof r[o] && (t[o] || (t[o] = [])).push(r[o]) + } + return t + } + + function N(e, t) { + for (var n in t) t.hasOwnProperty(n) && (e[n] = P(t[n].concat(e[n] || Q), "getDefaultProps" === n || "getInitialState" === n || "getChildContext" === n)) + } + + function O(e) { + for (var t in e) { + var n = e[t]; + "function" != typeof n || n.__bound || U.hasOwnProperty(t) || ((e[t] = n.bind(e)).__bound = !0) + } + } + + function M(e, t, n) { + if ("string" == typeof t && (t = e.constructor.prototype[t]), "function" == typeof t) return t.apply(e, n) + } + + function P(e, t) { + return function () { + for (var n, r = arguments, o = this, i = 0; i < e.length; i++) { + var a = M(o, e[i], r); + if (t && null != a) { + n || (n = {}); + for (var u in a) a.hasOwnProperty(u) && (n[u] = a[u]) + } else void 0 !== a && (n = a) + } + return n + } + } + + function T(e, t) { + A.call(this, e, t), this.componentWillReceiveProps = P([A, this.componentWillReceiveProps || "componentWillReceiveProps"]), this.render = P([A, j, this.render || "render", D]) + } + + function A(e, t) { + if (e) { + var n = e.children; + if (n && Array.isArray(n) && 1 === n.length && (e.children = n[0], e.children && "object" == typeof e.children && (e.children.length = 1, e.children[0] = e.children)), V) { + var r = "function" == typeof this ? this : this.constructor, o = this.propTypes || r.propTypes, + i = this.displayName || r.name; + o && z.a.checkPropTypes(o, e, "prop", i) + } + } + } + + function j(e) { + Z = this + } + + function D() { + Z === this && (Z = null) + } + + function I(e, t, n) { + R.Component.call(this, e, t), this.state = this.getInitialState ? this.getInitialState() : {}, this.refs = {}, this._refProxies = {}, n !== X && T.call(this, e, t) + } + + function L(e, t) { + I.call(this, e, t) + } + + n.d(t, "version", function () { + return G + }), n.d(t, "DOM", function () { + return te + }), n.d(t, "Children", function () { + return ee + }), n.d(t, "render", function () { + return a + }), n.d(t, "createClass", function () { + return C + }), n.d(t, "createFactory", function () { + return c + }), n.d(t, "createElement", function () { + return h + }), n.d(t, "cloneElement", function () { + return v + }), n.d(t, "isValidElement", function () { + return y + }), n.d(t, "findDOMNode", function () { + return S + }), n.d(t, "unmountComponentAtNode", function () { + return s + }), n.d(t, "Component", function () { + return I + }), n.d(t, "PureComponent", function () { + return L + }), n.d(t, "unstable_renderSubtreeIntoContainer", function () { + return u + }); + var F = n(86), z = n.n(F), R = n(93); + n.n(R); + n.d(t, "PropTypes", function () { + return z.a + }); + var G = "15.1.0", + B = "a abbr address area article aside audio b base bdi bdo big blockquote body br button canvas caption cite code col colgroup data datalist dd del details dfn dialog div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 head header hgroup hr html i iframe img input ins kbd keygen label legend li link main map mark menu menuitem meta meter nav noscript object ol optgroup option output p param picture pre progress q rp rt ruby s samp script section select small source span strong style sub summary sup table tbody td textarea tfoot th thead time title tr track u ul var video wbr circle clipPath defs ellipse g image line linearGradient mask path pattern polygon polyline radialGradient rect stop svg text tspan".split(" "), + H = "undefined" != typeof Symbol && Symbol.for && Symbol.for("react.element") || 60103, + $ = "undefined" != typeof Symbol ? Symbol.for("__preactCompatWrapper") : "__preactCompatWrapper", U = { + constructor: 1, + render: 1, + shouldComponentUpdate: 1, + componentWillReceiveProps: 1, + componentWillUpdate: 1, + componentDidUpdate: 1, + componentWillMount: 1, + componentDidMount: 1, + componentWillUnmount: 1, + componentDidUnmount: 1 + }, + W = /^(?:accent|alignment|arabic|baseline|cap|clip|color|fill|flood|font|glyph|horiz|marker|overline|paint|stop|strikethrough|stroke|text|underline|unicode|units|v|vert|word|writing|x)[A-Z]/, + X = {}, V = void 0 === e || !e.env || "production" !== e.env.NODE_ENV, Y = R.h("a", null).constructor; + Y.prototype.$$typeof = H, Y.prototype.preactCompatUpgraded = !1, Y.prototype.preactCompatNormalized = !1, Object.defineProperty(Y.prototype, "type", { + get: function () { + return this.nodeName + }, set: function (e) { + this.nodeName = e + }, configurable: !0 + }), Object.defineProperty(Y.prototype, "props", { + get: function () { + return this.attributes + }, set: function (e) { + this.attributes = e + }, configurable: !0 + }); + var q = R.options.event; + R.options.event = function (e) { + return q && (e = q(e)), e.persist = Object, e.nativeEvent = e, e + }; + var K = R.options.vnode; + R.options.vnode = function (e) { + if (!e.preactCompatUpgraded) { + e.preactCompatUpgraded = !0; + var t = e.nodeName, n = e.attributes = _({}, e.attributes); + "function" == typeof t ? (!0 === t[$] || t.prototype && "isReactComponent" in t.prototype) && (e.children && "" === String(e.children) && (e.children = void 0), e.children && (n.children = e.children), e.preactCompatNormalized || m(e), o(e)) : (e.children && "" === String(e.children) && (e.children = void 0), e.children && (n.children = e.children), n.defaultValue && (n.value || 0 === n.value || (n.value = n.defaultValue), delete n.defaultValue), i(e, n)) + } + K && K(e) + }; + var J = function () { + }; + J.prototype.getChildContext = function () { + return this.props.context + }, J.prototype.render = function (e) { + return e.children[0] + }; + for (var Z, Q = [], ee = { + map: function (e, t, n) { + return null == e ? null : (e = ee.toArray(e), n && n !== e && (t = t.bind(n)), e.map(t)) + }, forEach: function (e, t, n) { + if (null == e) return null; + e = ee.toArray(e), n && n !== e && (t = t.bind(n)), e.forEach(t) + }, count: function (e) { + return e && e.length || 0 + }, only: function (e) { + if (e = ee.toArray(e), 1 !== e.length) throw new Error("Children.only() expects only one child."); + return e[0] + }, toArray: function (e) { + return null == e ? [] : Array.isArray && Array.isArray(e) ? e : Q.concat(e) + } + }, te = {}, ne = B.length; ne--;) te[B[ne]] = c(B[ne]); + _(I.prototype = new R.Component, { + constructor: I, isReactComponent: {}, replaceState: function (e, t) { + var n = this; + this.setState(e, t); + for (var r in n.state) r in e || delete n.state[r] + }, getDOMNode: function () { + return this.base + }, isMounted: function () { + return !!this.base + } + }), E.prototype = I.prototype, L.prototype = new E, L.prototype.isPureReactComponent = !0, L.prototype.shouldComponentUpdate = function (e, t) { + return x(this.props, e) || x(this.state, t) + }; + var re = { + version: G, + DOM: te, + PropTypes: z.a, + Children: ee, + render: a, + createClass: C, + createFactory: c, + createElement: h, + cloneElement: v, + isValidElement: y, + findDOMNode: S, + unmountComponentAtNode: s, + Component: I, + PureComponent: L, + unstable_renderSubtreeIntoContainer: u + }; + t.default = re + }.call(t, n(9)) + }, function (e, t, n) { + var r = n(10); + e.exports = function (e) { + if (!r(e)) throw TypeError(e + " is not an object!"); + return e + } + }, function (e, t, n) { + var r = n(6), o = n(48), i = n(32), a = Object.defineProperty; + t.f = n(8) ? Object.defineProperty : function (e, t, n) { + if (r(e), t = i(t, !0), r(n), o) try { + return a(e, t, n) + } catch (e) { + } + if ("get" in n || "set" in n) throw TypeError("Accessors not supported!"); + return "value" in n && (e[t] = n.value), e + } + }, function (e, t, n) { + e.exports = !n(14)(function () { + return 7 != Object.defineProperty({}, "a", { + get: function () { + return 7 + } + }).a + }) + }, function (e, t) { + function n() { + throw new Error("setTimeout has not been defined") + } + + function r() { + throw new Error("clearTimeout has not been defined") + } + + function o(e) { + if (l === setTimeout) return setTimeout(e, 0); + if ((l === n || !l) && setTimeout) return l = setTimeout, setTimeout(e, 0); + try { + return l(e, 0) + } catch (t) { + try { + return l.call(null, e, 0) + } catch (t) { + return l.call(this, e, 0) + } + } + } + + function i(e) { + if (f === clearTimeout) return clearTimeout(e); + if ((f === r || !f) && clearTimeout) return f = clearTimeout, clearTimeout(e); + try { + return f(e) + } catch (t) { + try { + return f.call(null, e) + } catch (t) { + return f.call(this, e) + } + } + } + + function a() { + m && d && (m = !1, d.length ? h = d.concat(h) : v = -1, h.length && u()) + } + + function u() { + if (!m) { + var e = o(a); + m = !0; + for (var t = h.length; t;) { + for (d = h, h = []; ++v < t;) d && d[v].run(); + v = -1, t = h.length + } + d = null, m = !1, i(e) + } + } + + function s(e, t) { + this.fun = e, this.array = t + } + + function c() { + } + + var l, f, p = e.exports = {}; + !function () { + try { + l = "function" == typeof setTimeout ? setTimeout : n + } catch (e) { + l = n + } + try { + f = "function" == typeof clearTimeout ? clearTimeout : r + } catch (e) { + f = r + } + }(); + var d, h = [], m = !1, v = -1; + p.nextTick = function (e) { + var t = new Array(arguments.length - 1); + if (arguments.length > 1) for (var n = 1; n < arguments.length; n++) t[n - 1] = arguments[n]; + h.push(new s(e, t)), 1 !== h.length || m || o(u) + }, s.prototype.run = function () { + this.fun.apply(null, this.array) + }, p.title = "browser", p.browser = !0, p.env = {}, p.argv = [], p.version = "", p.versions = {}, p.on = c, p.addListener = c, p.once = c, p.off = c, p.removeListener = c, p.removeAllListeners = c, p.emit = c, p.prependListener = c, p.prependOnceListener = c, p.listeners = function (e) { + return [] + }, p.binding = function (e) { + throw new Error("process.binding is not supported") + }, p.cwd = function () { + return "/" + }, p.chdir = function (e) { + throw new Error("process.chdir is not supported") + }, p.umask = function () { + return 0 + } + }, function (e, t) { + e.exports = function (e) { + return "object" == typeof e ? null !== e : "function" == typeof e + } + }, function (e, t, n) { + var r = n(7), o = n(19); + e.exports = n(8) ? function (e, t, n) { + return r.f(e, t, o(1, n)) + } : function (e, t, n) { + return e[t] = n, e + } + }, function (e, t) { + var n = {}.hasOwnProperty; + e.exports = function (e, t) { + return n.call(e, t) + } + }, function (e, t, n) { + var r = n(24); + e.exports = function (e, t, n) { + if (r(e), void 0 === t) return e; + switch (n) { + case 1: + return function (n) { + return e.call(t, n) + }; + case 2: + return function (n, r) { + return e.call(t, n, r) + }; + case 3: + return function (n, r, o) { + return e.call(t, n, r, o) + } + } + return function () { + return e.apply(t, arguments) + } + } + }, function (e, t) { + e.exports = function (e) { + try { + return !!e() + } catch (e) { + return !0 + } + } + }, function (e, t) { + e.exports = {} + }, function (e, t, n) { + var r = n(54), o = n(35); + e.exports = function (e) { + return r(o(e)) + } + }, function (e, t, n) { + var r = n(35); + e.exports = function (e) { + return Object(r(e)) + } + }, function (e, t, n) { + function r(e, t) { + if (l(e)) return new Date(e.getTime()); + if ("string" != typeof e) return new Date(e); + var n = t || {}, r = n.additionalDigits; + r = null == r ? d : Number(r); + var c = o(e), f = i(c.date, r), h = f.year, m = f.restDateString, v = a(m, h); + if (v) { + var y, g = v.getTime(), b = 0; + return c.time && (b = u(c.time)), c.timezone ? y = s(c.timezone) : (y = new Date(g + b).getTimezoneOffset(), y = new Date(g + b + y * p).getTimezoneOffset()), new Date(g + b + y * p) + } + return new Date(e) + } + + function o(e) { + var t, n = {}, r = e.split(h); + if (m.test(r[0]) ? (n.date = null, t = r[0]) : (n.date = r[0], t = r[1]), t) { + var o = O.exec(t); + o ? (n.time = t.replace(o[1], ""), n.timezone = o[1]) : n.time = t + } + return n + } + + function i(e, t) { + var n, r = y[t], o = b[t]; + if (n = g.exec(e) || o.exec(e)) { + var i = n[1]; + return {year: parseInt(i, 10), restDateString: e.slice(i.length)} + } + if (n = v.exec(e) || r.exec(e)) { + var a = n[1]; + return {year: 100 * parseInt(a, 10), restDateString: e.slice(a.length)} + } + return {year: null} + } + + function a(e, t) { + if (null === t) return null; + var n, r, o, i; + if (0 === e.length) return r = new Date(0), r.setUTCFullYear(t), r; + if (n = w.exec(e)) return r = new Date(0), o = parseInt(n[1], 10) - 1, r.setUTCFullYear(t, o), r; + if (n = _.exec(e)) { + r = new Date(0); + var a = parseInt(n[1], 10); + return r.setUTCFullYear(t, 0, a), r + } + if (n = x.exec(e)) { + r = new Date(0), o = parseInt(n[1], 10) - 1; + var u = parseInt(n[2], 10); + return r.setUTCFullYear(t, o, u), r + } + if (n = S.exec(e)) return i = parseInt(n[1], 10) - 1, c(t, i); + if (n = E.exec(e)) { + i = parseInt(n[1], 10) - 1; + return c(t, i, parseInt(n[2], 10) - 1) + } + return null + } + + function u(e) { + var t, n, r; + if (t = C.exec(e)) return (n = parseFloat(t[1].replace(",", "."))) % 24 * f; + if (t = k.exec(e)) return n = parseInt(t[1], 10), r = parseFloat(t[2].replace(",", ".")), n % 24 * f + r * p; + if (t = N.exec(e)) { + n = parseInt(t[1], 10), r = parseInt(t[2], 10); + var o = parseFloat(t[3].replace(",", ".")); + return n % 24 * f + r * p + 1e3 * o + } + return null + } + + function s(e) { + var t, n; + return (t = M.exec(e)) ? 0 : (t = P.exec(e)) ? (n = 60 * parseInt(t[2], 10), "+" === t[1] ? -n : n) : (t = T.exec(e), t ? (n = 60 * parseInt(t[2], 10) + parseInt(t[3], 10), "+" === t[1] ? -n : n) : 0) + } + + function c(e, t, n) { + t = t || 0, n = n || 0; + var r = new Date(0); + r.setUTCFullYear(e, 0, 4); + var o = r.getUTCDay() || 7, i = 7 * t + n + 1 - o; + return r.setUTCDate(r.getUTCDate() + i), r + } + + var l = n(204), f = 36e5, p = 6e4, d = 2, h = /[T ]/, m = /:/, v = /^(\d{2})$/, + y = [/^([+-]\d{2})$/, /^([+-]\d{3})$/, /^([+-]\d{4})$/], g = /^(\d{4})/, + b = [/^([+-]\d{4})/, /^([+-]\d{5})/, /^([+-]\d{6})/], w = /^-(\d{2})$/, _ = /^-?(\d{3})$/, + x = /^-?(\d{2})-?(\d{2})$/, S = /^-?W(\d{2})$/, E = /^-?W(\d{2})-?(\d{1})$/, C = /^(\d{2}([.,]\d*)?)$/, + k = /^(\d{2}):?(\d{2}([.,]\d*)?)$/, N = /^(\d{2}):?(\d{2}):?(\d{2}([.,]\d*)?)$/, O = /([Z+-].*)$/, + M = /^(Z)$/, P = /^([+-])(\d{2})$/, T = /^([+-])(\d{2}):?(\d{2})$/; + e.exports = r + }, function (e, t) { + e.exports = function (e, t) { + return {enumerable: !(1 & e), configurable: !(2 & e), writable: !(4 & e), value: t} + } + }, function (e, t, n) { + "use strict"; + var r = n(101)(!0); + n(51)(String, "String", function (e) { + this._t = String(e), this._i = 0 + }, function () { + var e, t = this._t, n = this._i; + return n >= t.length ? {value: void 0, done: !0} : (e = r(t, n), this._i += e.length, {value: e, done: !1}) + }) + }, function (e, t) { + e.exports = !0 + }, function (e, t, n) { + var r = n(53), o = n(40); + e.exports = Object.keys || function (e) { + return r(e, o) + } + }, function (e, t) { + var n = {}.toString; + e.exports = function (e) { + return n.call(e).slice(8, -1) + } + }, function (e, t) { + e.exports = function (e) { + if ("function" != typeof e) throw TypeError(e + " is not a function!"); + return e + } + }, function (e, t) { + var n = 0, r = Math.random(); + e.exports = function (e) { + return "Symbol(".concat(void 0 === e ? "" : e, ")_", (++n + r).toString(36)) + } + }, function (e, t, n) { + var r = n(7).f, o = n(12), i = n(2)("toStringTag"); + e.exports = function (e, t, n) { + e && !o(e = n ? e : e.prototype, i) && r(e, i, {configurable: !0, value: t}) + } + }, function (e, t, n) { + n(106); + for (var r = n(1), o = n(11), i = n(15), a = n(2)("toStringTag"), u = "CSSRuleList,CSSStyleDeclaration,CSSValueList,ClientRectList,DOMRectList,DOMStringList,DOMTokenList,DataTransferItemList,FileList,HTMLAllCollection,HTMLCollection,HTMLFormElement,HTMLSelectElement,MediaList,MimeTypeArray,NamedNodeMap,NodeList,PaintRequestList,Plugin,PluginArray,SVGLengthList,SVGNumberList,SVGPathSegList,SVGPointList,SVGStringList,SVGTransformList,SourceBufferList,StyleSheetList,TextTrackCueList,TextTrackList,TouchList".split(","), s = 0; s < u.length; s++) { + var c = u[s], l = r[c], f = l && l.prototype; + f && !f[a] && o(f, a, c), i[c] = i.Array + } + }, function (e, t) { + t.f = {}.propertyIsEnumerable + }, function (e, t, n) { + "use strict"; + t.__esModule = !0, t.default = function (e, t) { + if (!(e instanceof t)) throw new TypeError("Cannot call a class as a function") + } + }, function (e, t, n) { + "use strict"; + t.__esModule = !0; + var r = n(83), o = function (e) { + return e && e.__esModule ? e : {default: e} + }(r); + t.default = function () { + function e(e, t) { + for (var n = 0; n < t.length; n++) { + var r = t[n]; + r.enumerable = r.enumerable || !1, r.configurable = !0, "value" in r && (r.writable = !0), (0, o.default)(e, r.key, r) + } + } + + return function (t, n, r) { + return n && e(t.prototype, n), r && e(t, r), t + } + }() + }, function (e, t, n) { + var r = n(10), o = n(1).document, i = r(o) && r(o.createElement); + e.exports = function (e) { + return i ? o.createElement(e) : {} + } + }, function (e, t, n) { + var r = n(10); + e.exports = function (e, t) { + if (!r(e)) return e; + var n, o; + if (t && "function" == typeof (n = e.toString) && !r(o = n.call(e))) return o; + if ("function" == typeof (n = e.valueOf) && !r(o = n.call(e))) return o; + if (!t && "function" == typeof (n = e.toString) && !r(o = n.call(e))) return o; + throw TypeError("Can't convert object to primitive value") + } + }, function (e, t, n) { + "use strict"; + e.exports = "SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED" + }, function (e, t) { + var n = Math.ceil, r = Math.floor; + e.exports = function (e) { + return isNaN(e = +e) ? 0 : (e > 0 ? r : n)(e) + } + }, function (e, t) { + e.exports = function (e) { + if (void 0 == e) throw TypeError("Can't call method on " + e); + return e + } + }, function (e, t, n) { + var r = n(6), o = n(103), i = n(40), a = n(38)("IE_PROTO"), u = function () { + }, s = function () { + var e, t = n(31)("iframe"), r = i.length; + for (t.style.display = "none", n(55).appendChild(t), t.src = "javascript:", e = t.contentWindow.document, e.open(), e.write("