mirror of
https://github.com/zeromicro/zero-doc.git
synced 2025-01-23 01:30:28 +08:00
merage go-zero-doc
This commit is contained in:
parent
b31059e665
commit
19e981f8f0
23
.github/workflows/build.sh
vendored
Normal file
23
.github/workflows/build.sh
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
ACCESSTOKEN=$1
|
||||
REPO="https://x-access-token:${ACCESSTOKEN}@github.com/zeromicro/go-zero-pages.git"
|
||||
|
||||
# git 配置
|
||||
echo "git基础配置"
|
||||
git config --global user.name "anqiansong"
|
||||
git config --global user.email "anqiansong@tal.com"
|
||||
|
||||
# push
|
||||
cd ./go-zero.dev
|
||||
mkdir ./doc
|
||||
cd ./doc
|
||||
echo $PWD
|
||||
echo "document clone..."
|
||||
git clone ${REPO}
|
||||
cd go-zero-pages
|
||||
rm -rf ./*
|
||||
cp -rf ../../_book/* .
|
||||
git add ./*
|
||||
git commit -m 'update document'
|
||||
echo "document push..."
|
||||
git push -f
|
45
.github/workflows/build.yml
vendored
Normal file
45
.github/workflows/build.yml
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
# 当master有代码提交或者有pr时,自动build gitbook,并覆盖git@github.com:zeromicro/go-zero.git仓库中内容
|
||||
# 以实现自动发布文档
|
||||
|
||||
name: Document Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: enter work dir
|
||||
run: cd go-zero.dev
|
||||
- name: checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: chmod contributor tool
|
||||
run: chmod +x ../.github/workflows/contributor-tool
|
||||
- name: get latest contributors(cn)
|
||||
run: ../.github/workflows/contributor-tool -f contributor -l zh -o ./cn/contributor.md
|
||||
- name: get latest contributors(en)
|
||||
run: ../.github/workflows/contributor-tool -f contributor -l en -o ./en/contributor.md
|
||||
- name: use node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '12.18.1'
|
||||
- name: install gitbook
|
||||
run: npm install gitbook-cli -g
|
||||
- name: gitbook version
|
||||
run: gitbook --version
|
||||
- name: gitbook install
|
||||
run: gitbook install
|
||||
- name: build
|
||||
run: gitbook build
|
||||
- name: delete original index
|
||||
run: rm -f ./_book/index.html
|
||||
- name: build index
|
||||
run: ../.github/workflows/contributor-tool -i ./_book/index.html
|
||||
- name: chmod
|
||||
run: chmod +x ./.github/workflows/build.sh
|
||||
- name: publish
|
||||
run: ../.github/workflows/build.sh ${{ secrets.ACCESSTOKEN }}
|
||||
shell: bash
|
BIN
.github/workflows/contributor-tool
vendored
Executable file
BIN
.github/workflows/contributor-tool
vendored
Executable file
Binary file not shown.
2
LICENSE
2
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
|
||||
|
@ -1,5 +0,0 @@
|
||||
# 熔断机制设计
|
||||
|
||||
## 设计目的
|
||||
|
||||
* 依赖的服务出现大规模故障时,调用方应该尽可能少调用,降低故障服务的压力,使之尽快恢复服务
|
@ -1,314 +0,0 @@
|
||||
# Goctl Model
|
||||
|
||||
goctl model 为go-zero下的工具模块中的组件之一,目前支持识别mysql ddl进行model层代码生成,通过命令行或者idea插件(即将支持)可以有选择地生成带redis cache或者不带redis cache的代码逻辑。
|
||||
|
||||
## 快速开始
|
||||
|
||||
* 通过ddl生成
|
||||
|
||||
```shell script
|
||||
goctl model mysql ddl -src="./*.sql" -dir="./sql/model" -c=true
|
||||
```
|
||||
|
||||
执行上述命令后即可快速生成CURD代码。
|
||||
|
||||
```Plain Text
|
||||
model
|
||||
│ ├── error.go
|
||||
│ └── usermodel.go
|
||||
```
|
||||
|
||||
* 通过datasource生成
|
||||
|
||||
```shell script
|
||||
goctl model mysql datasource -url="user:password@tcp(127.0.0.1:3306)/database" -table="*" -dir="./model"
|
||||
```
|
||||
|
||||
* 生成代码示例
|
||||
|
||||
```go
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tal-tech/go-zero/core/stores/cache"
|
||||
"github.com/tal-tech/go-zero/core/stores/sqlc"
|
||||
"github.com/tal-tech/go-zero/core/stores/sqlx"
|
||||
"github.com/tal-tech/go-zero/core/stringx"
|
||||
"github.com/tal-tech/go-zero/tools/goctl/model/sql/builderx"
|
||||
)
|
||||
|
||||
var (
|
||||
userFieldNames = builderx.FieldNames(&User{})
|
||||
userRows = strings.Join(userFieldNames, ",")
|
||||
userRowsExpectAutoSet = strings.Join(stringx.Remove(userFieldNames, "id", "create_time", "update_time"), ",")
|
||||
userRowsWithPlaceHolder = strings.Join(stringx.Remove(userFieldNames, "id", "create_time", "update_time"), "=?,") + "=?"
|
||||
|
||||
cacheUserIdPrefix = "cache#User#id#"
|
||||
cacheUserNamePrefix = "cache#User#name#"
|
||||
cacheUserMobilePrefix = "cache#User#mobile#"
|
||||
)
|
||||
|
||||
type (
|
||||
UserModel struct {
|
||||
sqlc.CachedConn
|
||||
table string
|
||||
}
|
||||
|
||||
User struct {
|
||||
Id int64 `db:"id"`
|
||||
Name string `db:"name"` // 用户名称
|
||||
Password string `db:"password"` // 用户密码
|
||||
Mobile string `db:"mobile"` // 手机号
|
||||
Gender string `db:"gender"` // 男|女|未公开
|
||||
Nickname string `db:"nickname"` // 用户昵称
|
||||
CreateTime time.Time `db:"create_time"`
|
||||
UpdateTime time.Time `db:"update_time"`
|
||||
}
|
||||
)
|
||||
|
||||
func NewUserModel(conn sqlx.SqlConn, c cache.CacheConf) *UserModel {
|
||||
return &UserModel{
|
||||
CachedConn: sqlc.NewConn(conn, c),
|
||||
table: "user",
|
||||
}
|
||||
}
|
||||
|
||||
func (m *UserModel) Insert(data User) (sql.Result, error) {
|
||||
userNameKey := fmt.Sprintf("%s%v", cacheUserNamePrefix, data.Name)
|
||||
userMobileKey := fmt.Sprintf("%s%v", cacheUserMobilePrefix, data.Mobile)
|
||||
ret, err := m.Exec(func(conn sqlx.SqlConn) (result sql.Result, err error) {
|
||||
query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?)", m.table, userRowsExpectAutoSet)
|
||||
return conn.Exec(query, data.Name, data.Password, data.Mobile, data.Gender, data.Nickname)
|
||||
}, userNameKey, userMobileKey)
|
||||
return ret, err
|
||||
}
|
||||
|
||||
func (m *UserModel) FindOne(id int64) (*User, error) {
|
||||
userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, id)
|
||||
var resp User
|
||||
err := m.QueryRow(&resp, userIdKey, func(conn sqlx.SqlConn, v interface{}) error {
|
||||
query := fmt.Sprintf("select %s from %s where id = ? limit 1", userRows, m.table)
|
||||
return conn.QueryRow(v, query, id)
|
||||
})
|
||||
switch err {
|
||||
case nil:
|
||||
return &resp, nil
|
||||
case sqlc.ErrNotFound:
|
||||
return nil, ErrNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (m *UserModel) FindOneByName(name string) (*User, error) {
|
||||
userNameKey := fmt.Sprintf("%s%v", cacheUserNamePrefix, name)
|
||||
var resp User
|
||||
err := m.QueryRowIndex(&resp, userNameKey, m.formatPrimary, func(conn sqlx.SqlConn, v interface{}) (i interface{}, e error) {
|
||||
query := fmt.Sprintf("select %s from %s where name = ? limit 1", userRows, m.table)
|
||||
if err := conn.QueryRow(&resp, query, name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Id, nil
|
||||
}, m.queryPrimary)
|
||||
switch err {
|
||||
case nil:
|
||||
return &resp, nil
|
||||
case sqlc.ErrNotFound:
|
||||
return nil, ErrNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (m *UserModel) FindOneByMobile(mobile string) (*User, error) {
|
||||
userMobileKey := fmt.Sprintf("%s%v", cacheUserMobilePrefix, mobile)
|
||||
var resp User
|
||||
err := m.QueryRowIndex(&resp, userMobileKey, m.formatPrimary, func(conn sqlx.SqlConn, v interface{}) (i interface{}, e error) {
|
||||
query := fmt.Sprintf("select %s from %s where mobile = ? limit 1", userRows, m.table)
|
||||
if err := conn.QueryRow(&resp, query, mobile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Id, nil
|
||||
}, m.queryPrimary)
|
||||
switch err {
|
||||
case nil:
|
||||
return &resp, nil
|
||||
case sqlc.ErrNotFound:
|
||||
return nil, ErrNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (m *UserModel) Update(data User) error {
|
||||
userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, data.Id)
|
||||
_, err := m.Exec(func(conn sqlx.SqlConn) (result sql.Result, err error) {
|
||||
query := fmt.Sprintf("update %s set %s where id = ?", m.table, userRowsWithPlaceHolder)
|
||||
return conn.Exec(query, data.Name, data.Password, data.Mobile, data.Gender, data.Nickname, data.Id)
|
||||
}, userIdKey)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *UserModel) Delete(id int64) error {
|
||||
data, err := m.FindOne(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userMobileKey := fmt.Sprintf("%s%v", cacheUserMobilePrefix, data.Mobile)
|
||||
userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, id)
|
||||
userNameKey := fmt.Sprintf("%s%v", cacheUserNamePrefix, data.Name)
|
||||
_, err = m.Exec(func(conn sqlx.SqlConn) (result sql.Result, err error) {
|
||||
query := fmt.Sprintf("delete from %s where id = ?", m.table)
|
||||
return conn.Exec(query, id)
|
||||
}, userMobileKey, userIdKey, userNameKey)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *UserModel) formatPrimary(primary interface{}) string {
|
||||
return fmt.Sprintf("%s%v", cacheUserIdPrefix, primary)
|
||||
}
|
||||
|
||||
func (m *UserModel) queryPrimary(conn sqlx.SqlConn, v, primary interface{}) error {
|
||||
query := fmt.Sprintf("select %s from %s where id = ? limit 1", userRows, m.table)
|
||||
return conn.QueryRow(v, query, primary)
|
||||
}
|
||||
```
|
||||
|
||||
## 用法
|
||||
|
||||
```Plain Text
|
||||
goctl model mysql -h
|
||||
```
|
||||
|
||||
```Plain Text
|
||||
NAME:
|
||||
goctl model mysql - generate mysql model"
|
||||
|
||||
USAGE:
|
||||
goctl model mysql command [command options] [arguments...]
|
||||
|
||||
COMMANDS:
|
||||
ddl generate mysql model from ddl"
|
||||
datasource generate model from datasource"
|
||||
|
||||
OPTIONS:
|
||||
--help, -h show help
|
||||
```
|
||||
|
||||
## 生成规则
|
||||
|
||||
* 默认规则
|
||||
|
||||
我们默认用户在建表时会创建createTime、updateTime字段(忽略大小写、下划线命名风格)且默认值均为`CURRENT_TIMESTAMP`,而updateTime支持`ON UPDATE CURRENT_TIMESTAMP`,对于这两个字段生成`insert`、`update`时会被移除,不在赋值范畴内,当然,如果你不需要这两个字段那也无大碍。
|
||||
* 带缓存模式
|
||||
* ddl
|
||||
|
||||
```shell script
|
||||
goctl model mysql -src={patterns} -dir={dir} -cache=true
|
||||
```
|
||||
|
||||
help
|
||||
|
||||
```
|
||||
NAME:
|
||||
goctl model mysql ddl - generate mysql model from ddl
|
||||
|
||||
USAGE:
|
||||
goctl model mysql ddl [command options] [arguments...]
|
||||
|
||||
OPTIONS:
|
||||
--src value, -s value the path or path globbing patterns of the ddl
|
||||
--dir value, -d value the target dir
|
||||
--style value the file naming style, lower|camel|underline,default is lower
|
||||
--cache, -c generate code with cache [optional]
|
||||
--idea for idea plugin [optional]
|
||||
|
||||
```
|
||||
|
||||
* datasource
|
||||
|
||||
```shell script
|
||||
goctl model mysql datasource -url={datasource} -table={patterns} -dir={dir} -cache=true
|
||||
```
|
||||
|
||||
help
|
||||
|
||||
```
|
||||
NAME:
|
||||
goctl model mysql datasource - generate model from datasource
|
||||
|
||||
USAGE:
|
||||
goctl model mysql datasource [command options] [arguments...]
|
||||
|
||||
OPTIONS:
|
||||
--url value the data source of database,like "root:password@tcp(127.0.0.1:3306)/database
|
||||
--table value, -t value the table or table globbing patterns in the database
|
||||
--cache, -c generate code with cache [optional]
|
||||
--dir value, -d value the target dir
|
||||
--style value the file naming style, lower|camel|snake, default is lower
|
||||
--idea for idea plugin [optional]
|
||||
|
||||
```
|
||||
|
||||
示例用法请参考[用法](./example/generator.sh)
|
||||
|
||||
> NOTE: goctl model mysql ddl/datasource 均新增了一个`--style`参数,用于标记文件命名风格。
|
||||
|
||||
目前仅支持redis缓存,如果选择带缓存模式,即生成的`FindOne(ByXxx)`&`Delete`代码会生成带缓存逻辑的代码,目前仅支持单索引字段(除全文索引外),对于联合索引我们默认认为不需要带缓存,且不属于通用型代码,因此没有放在代码生成行列,如example中user表中的`id`、`name`、`mobile`字段均属于单字段索引。
|
||||
|
||||
* 不带缓存模式
|
||||
|
||||
* ddl
|
||||
|
||||
```shell script
|
||||
goctl model -src={patterns} -dir={dir}
|
||||
```
|
||||
|
||||
* datasource
|
||||
|
||||
```shell script
|
||||
goctl model mysql datasource -url={datasource} -table={patterns} -dir={dir}
|
||||
```
|
||||
|
||||
or
|
||||
* ddl
|
||||
|
||||
```shell script
|
||||
goctl model -src={patterns} -dir={dir} -cache=false
|
||||
```
|
||||
|
||||
* datasource
|
||||
|
||||
```shell script
|
||||
goctl model mysql datasource -url={datasource} -table={patterns} -dir={dir} -cache=false
|
||||
```
|
||||
|
||||
生成代码仅基本的CURD结构。
|
||||
|
||||
## 缓存
|
||||
|
||||
对于缓存这一块我选择用一问一答的形式进行罗列。我想这样能够更清晰的描述model中缓存的功能。
|
||||
|
||||
* 缓存会缓存哪些信息?
|
||||
|
||||
对于主键字段缓存,会缓存整个结构体信息,而对于单索引字段(除全文索引)则缓存主键字段值。
|
||||
|
||||
* 数据有更新(`update`)操作会清空缓存吗?
|
||||
|
||||
会,但仅清空主键缓存的信息,why?这里就不做详细赘述了。
|
||||
|
||||
* 为什么不按照单索引字段生成`updateByXxx`和`deleteByXxx`的代码?
|
||||
|
||||
理论上是没任何问题,但是我们认为,对于model层的数据操作均是以整个结构体为单位,包括查询,我不建议只查询某部分字段(不反对),否则我们的缓存就没有意义了。
|
||||
|
||||
* 为什么不支持`findPageLimit`、`findAll`这么模式代码生层?
|
||||
|
||||
目前,我认为除了基本的CURD外,其他的代码均属于<i>业务型</i>代码,这个我觉得开发人员根据业务需要进行编写更好。
|
||||
|
244
doc/goctl.md
244
doc/goctl.md
@ -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)
|
136
doc/jwt.md
136
doc/jwt.md
@ -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了,一般都需要设置过期时间前几天。
|
||||
|
@ -1,15 +0,0 @@
|
||||
# PeriodicalExecutor设计
|
||||
|
||||
## 添加任务
|
||||
|
||||
* 当前没有未执行的任务
|
||||
* 添加并启动定时器
|
||||
* 已有未执行的任务
|
||||
* 添加并检查是否到达最大缓存数
|
||||
* 如到,执行所有缓存任务
|
||||
* 未到,只添加
|
||||
|
||||
## 定时器到期
|
||||
|
||||
* 清除并执行所有缓存任务
|
||||
* 再等待N个定时周期,如果等待过程中一直没有新任务,则退出
|
@ -1,3 +1,5 @@
|
||||
# DEPRECATED: PLEASE MOVE TO https://go-zero.dev/cn/extended-reading.html
|
||||
|
||||
# Rapid development of microservices
|
||||
|
||||
English | [简体中文](shorturl.md)
|
||||
@ -23,7 +25,7 @@ For any point listed above, we need a long article to describe the theory and th
|
||||
|
||||
As well, we always adhere to the idea that **prefer tools over conventions and documents**. We hope to reduce the boilerplate code as much as possible, and let developers focus on developing the business related code. For this purpose, we developed the tool `goctl`.
|
||||
|
||||
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
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
# DEPRECATED: PLEASE MOVE TO https://go-zero.dev/cn/extended-reading.html
|
||||
|
||||
# 快速构建高并发微服务
|
||||
|
||||
[English](shorturl-en.md) | 简体中文
|
||||
@ -23,7 +25,7 @@
|
||||
|
||||
另外,我们始终秉承 **工具大于约定和文档** 的理念。我们希望尽可能减少开发人员的心智负担,把精力都投入到产生业务价值的代码上,减少重复代码的编写,所以我们开发了 `goctl` 工具。
|
||||
|
||||
下面我通过短链微服务来演示通过 [go-zero](https://github.com/tal-tech/go-zero) 快速的创建微服务的流程,走完一遍,你就会发现:原来编写微服务如此简单!
|
||||
下面我通过短链微服务来演示通过 [go-zero](https://github.com/zeromicro/go-zero) 快速的创建微服务的流程,走完一遍,你就会发现:原来编写微服务如此简单!
|
||||
|
||||
## 1. 什么是短链服务
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
# DEPRECATED: PLEASE MOVE TO https://go-zero.dev
|
||||
---
|
||||
home: true
|
||||
heroImage: /logo.png
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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,简单使用方式如下
|
||||
|
@ -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)才出现,为什么会这么设计?
|
||||
|
||||
|
||||
|
||||
|
@ -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`:略
|
||||
|
||||
|
@ -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文件,然后将其添加到环境变量中。
|
||||
|
||||
|
||||
# 校验
|
||||
|
@ -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
|
||||
|
||||
|
@ -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]
|
||||
|
||||
```
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -156,7 +156,7 @@ return allowed
|
||||
## 参考
|
||||
|
||||
|
||||
- [go-zero tokenlimit](https://github.com/tal-tech/go-zero/blob/master/core/limit/tokenlimit.go)
|
||||
- [go-zero tokenlimit](https://github.com/zeromicro/go-zero/blob/master/core/limit/tokenlimit.go)
|
||||
- [Go-Redis 提供的分布式限流库](https://github.com/go-redis/redis_rate)
|
||||
|
||||
<Vssue title="tokenLimit" />
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
流数据处理在我们的日常工作中非常常见,举个例子,我们在业务开发中往往会记录许多业务日志,这些日志一般是先发送到 Kafka,然后再由 Job 消费 Kafaka 写到 elasticsearch,在进行日志流处理的过程中,往往还会对日志做一些处理,比如过滤无效的日志,做一些计算以及重新组合日志等等,示意图如下:
|
||||
![fx_log.png](https://cdn.nlark.com/yuque/0/2020/png/1220818/1602254714159-7753eb68-5e65-4194-94ad-197af105ed44.png#align=left&display=inline&height=766&margin=%5Bobject%20Object%5D&name=fx_log.png&originHeight=766&originWidth=1422&size=74150&status=done&style=none&width=1422)
|
||||
### 流处理工具fx
|
||||
[go-zero](https://github.com/tal-tech/go-zero)是一个功能完备的微服务框架,框架中内置了很多非常实用的工具,其中就包含流数据处理工具[fx](https://github.com/tal-tech/go-zero/tree/master/core/fx),下面我们通过一个简单的例子来认识下该工具:
|
||||
[go-zero](https://github.com/zeromicro/go-zero)是一个功能完备的微服务框架,框架中内置了很多非常实用的工具,其中就包含流数据处理工具[fx](https://github.com/zeromicro/go-zero/tree/master/core/fx),下面我们通过一个简单的例子来认识下该工具:
|
||||
```go
|
||||
package main
|
||||
|
||||
|
2
go-zero.dev/LANGS.md
Normal file
2
go-zero.dev/LANGS.md
Normal file
@ -0,0 +1,2 @@
|
||||
* [English](en)
|
||||
* [中文](cn)
|
3
go-zero.dev/README-EN.MD
Normal file
3
go-zero.dev/README-EN.MD
Normal file
@ -0,0 +1,3 @@
|
||||
# go-zero.dev
|
||||
This directory is the gitbook source for the official document https://go-zero.dev, when you prepare to pull a request
|
||||
into master, [anqiansong](https://github.com/anqiansong) is one of reviewer must be assigned to. *IMPORT!!!*
|
3
go-zero.dev/README.MD
Normal file
3
go-zero.dev/README.MD
Normal file
@ -0,0 +1,3 @@
|
||||
# go-zero.dev
|
||||
本目录主是官方文档 https://go-zero.dev 的源文档,修改后 pr 请 assign [anqiansong](https://github.com/anqiansong)
|
||||
进行 review,切记!
|
90
go-zero.dev/book.json
Normal file
90
go-zero.dev/book.json
Normal file
@ -0,0 +1,90 @@
|
||||
{
|
||||
"title": "go-zero document",
|
||||
"author": "anqiansong",
|
||||
"description": "Golang 微服务框架 | 集成各种工程实践的 WEB 和 RPC 框架 | 一键生成 Go, iOS, Android, Kotlin, Dart, TypeScript, JavaScript 代码",
|
||||
"language": "zh-hans",
|
||||
"gitbook": "3.2.3",
|
||||
"plugins": [
|
||||
"back-to-top-button",
|
||||
"chapter-fold",
|
||||
"code",
|
||||
"-lunr",
|
||||
"-search",
|
||||
"search-pro",
|
||||
"github",
|
||||
"splitter",
|
||||
"-sharing",
|
||||
"sharing-plus",
|
||||
"tbfed-pagefooter",
|
||||
"flexible-alerts",
|
||||
"page-toc-button",
|
||||
"pageview-count",
|
||||
"popup",
|
||||
"hide-element",
|
||||
"edit-link",
|
||||
"-highlight",
|
||||
"prism",
|
||||
"theme-comscore"
|
||||
],
|
||||
"pluginsConfig": {
|
||||
"prism": {
|
||||
"lang": {
|
||||
"shell": "bash"
|
||||
},
|
||||
"css": [
|
||||
"prismjs/themes/prism-tomorrow.css"
|
||||
]
|
||||
},
|
||||
"github": {
|
||||
"url": "https://github.com/zeromicro/go-zero"
|
||||
},
|
||||
"page-toc-button": {
|
||||
"maxTocDepth": 2,
|
||||
"minTocSize": 2
|
||||
},
|
||||
"tbfed-pagefooter": {
|
||||
"copyright": "Copyright © 2019-2021 go-zero",
|
||||
"modify_label": "Last UpdateTime:",
|
||||
"modify_format": "YYYY-MM-DD HH:mm:ss"
|
||||
},
|
||||
"hide-element": {
|
||||
"elements": [
|
||||
".gitbook-link"
|
||||
]
|
||||
},
|
||||
"edit-link": {
|
||||
"base": "https://github.com/zeromicro/zero-doc/tree/main/go-zero.dev",
|
||||
"label": "EDIT THIS PAGE"
|
||||
},
|
||||
"sharing": {
|
||||
"douban": false,
|
||||
"facebook": false,
|
||||
"google": true,
|
||||
"hatenaBookmark": false,
|
||||
"instapaper": false,
|
||||
"line": true,
|
||||
"linkedin": true,
|
||||
"messenger": false,
|
||||
"pocket": false,
|
||||
"qq": false,
|
||||
"qzone": true,
|
||||
"stumbleupon": false,
|
||||
"twitter": false,
|
||||
"viber": false,
|
||||
"vk": false,
|
||||
"weibo": true,
|
||||
"whatsapp": false,
|
||||
"all": [
|
||||
"douban",
|
||||
"facebook",
|
||||
"google",
|
||||
"linkedin",
|
||||
"twitter",
|
||||
"weibo",
|
||||
"qq",
|
||||
"qzone",
|
||||
"weibo"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
218
go-zero.dev/cn/README.md
Normal file
218
go-zero.dev/cn/README.md
Normal file
@ -0,0 +1,218 @@
|
||||
<img align="right" width="150px" src="https://gitee.com/kevwan/static/raw/master/doc/images/go-zero.png">
|
||||
|
||||
# go-zero
|
||||
|
||||
[![Go](https://github.com/zeromicro/go-zero/workflows/Go/badge.svg?branch=master)](https://github.com/tal-tech/go-zero/actions)
|
||||
[![Go Report Card](https://goreportcard.com/badge/github.com/tal-tech/go-zero)](https://goreportcard.com/report/github.com/tal-tech/go-zero)
|
||||
[![goproxy](https://goproxy.cn/stats/github.com/tal-tech/go-zero/badges/download-count.svg)](https://goproxy.cn/stats/github.com/tal-tech/go-zero/badges/download-count.svg)
|
||||
[![codecov](https://codecov.io/gh/tal-tech/go-zero/branch/master/graph/badge.svg)](https://codecov.io/gh/tal-tech/go-zero)
|
||||
[![Release](https://img.shields.io/github/v/release/tal-tech/go-zero.svg?style=flat-square)](https://github.com/tal-tech/go-zero)
|
||||
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
|
||||
|
||||
## 0. go-zero 介绍
|
||||
|
||||
go-zero 是一个集成了各种工程实践的 web 和 rpc 框架。通过弹性设计保障了大并发服务端的稳定性,经受了充分的实战检验。
|
||||
|
||||
go-zero 包含极简的 API 定义和生成工具 goctl,可以根据定义的 api 文件一键生成 Go, iOS, Android, Kotlin, Dart, TypeScript, JavaScript 代码,并可直接运行。
|
||||
|
||||
使用 go-zero 的好处:
|
||||
|
||||
* 轻松获得支撑千万日活服务的稳定性
|
||||
* 内建级联超时控制、限流、自适应熔断、自适应降载等微服务治理能力,无需配置和额外代码
|
||||
* 微服务治理中间件可无缝集成到其它现有框架使用
|
||||
* 极简的 API 描述,一键生成各端代码
|
||||
* 自动校验客户端请求参数合法性
|
||||
* 大量微服务治理和并发工具包
|
||||
|
||||
<img src="https://gitee.com/kevwan/static/raw/master/doc/images/architecture.png" alt="架构图" width="1500" />
|
||||
|
||||
## 1. go-zero 框架背景
|
||||
|
||||
18 年初,我们决定从 `Java+MongoDB` 的单体架构迁移到微服务架构,经过仔细思考和对比,我们决定:
|
||||
|
||||
* 基于 Go 语言
|
||||
* 高效的性能
|
||||
* 简洁的语法
|
||||
* 广泛验证的工程效率
|
||||
* 极致的部署体验
|
||||
* 极低的服务端资源成本
|
||||
* 自研微服务框架
|
||||
* 有过很多微服务框架自研经验
|
||||
* 需要有更快速的问题定位能力
|
||||
* 更便捷的增加新特性
|
||||
|
||||
## 2. go-zero 框架设计思考
|
||||
|
||||
对于微服务框架的设计,我们期望保障微服务稳定性的同时,也要特别注重研发效率。所以设计之初,我们就有如下一些准则:
|
||||
|
||||
* 保持简单,第一原则
|
||||
* 弹性设计,面向故障编程
|
||||
* 工具大于约定和文档
|
||||
* 高可用
|
||||
* 高并发
|
||||
* 易扩展
|
||||
* 对业务开发友好,封装复杂度
|
||||
* 约束做一件事只有一种方式
|
||||
|
||||
我们经历不到半年时间,彻底完成了从 `Java+MongoDB` 到 `Golang+MySQL` 为主的微服务体系迁移,并于 18 年 8 月底完全上线,稳定保障了业务后续迅速增长,确保了整个服务的高可用。
|
||||
|
||||
## 3. go-zero 项目实现和特点
|
||||
|
||||
go-zero 是一个集成了各种工程实践的包含 web 和 rpc 框架,有如下主要特点:
|
||||
|
||||
* 强大的工具支持,尽可能少的代码编写
|
||||
* 极简的接口
|
||||
* 完全兼容 net/http
|
||||
* 支持中间件,方便扩展
|
||||
* 高性能
|
||||
* 面向故障编程,弹性设计
|
||||
* 内建服务发现、负载均衡
|
||||
* 内建限流、熔断、降载,且自动触发,自动恢复
|
||||
* API 参数自动校验
|
||||
* 超时级联控制
|
||||
* 自动缓存控制
|
||||
* 链路跟踪、统计报警等
|
||||
* 高并发支撑,稳定保障了疫情期间每天的流量洪峰
|
||||
|
||||
如下图,我们从多个层面保障了整体服务的高可用:
|
||||
|
||||
![弹性设计](https://gitee.com/kevwan/static/raw/master/doc/images/resilience.jpg)
|
||||
|
||||
觉得不错的话,别忘 **star** 👏
|
||||
|
||||
## 4. Installation
|
||||
|
||||
在项目目录下通过如下命令安装:
|
||||
|
||||
```shell
|
||||
GO111MODULE=on GOPROXY=https://goproxy.cn/,direct go get -u github.com/tal-tech/go-zero
|
||||
```
|
||||
|
||||
## 5. Quick Start
|
||||
|
||||
0. 完整示例请查看
|
||||
|
||||
[快速构建高并发微服务](https://github.com/tal-tech/zero-doc/blob/main/doc/shorturl.md)
|
||||
|
||||
[快速构建高并发微服务 - 多 RPC 版](https://github.com/tal-tech/zero-doc/blob/main/docs/zero/bookstore.md)
|
||||
|
||||
1. 安装 goctl 工具
|
||||
|
||||
`goctl` 读作 `go control`,不要读成 `go C-T-L`。`goctl` 的意思是不要被代码控制,而是要去控制它。其中的 `go` 不是指 `golang`。在设计 `goctl` 之初,我就希望通过 ` 她 `
|
||||
来解放我们的双手👈
|
||||
|
||||
```shell
|
||||
GO111MODULE=on GOPROXY=https://goproxy.cn/,direct go get -u github.com/tal-tech/go-zero/tools/goctl
|
||||
```
|
||||
|
||||
如果使用 go1.16 版本, 可以使用 `go install` 命令安装
|
||||
|
||||
```shell
|
||||
GOPROXY=https://goproxy.cn/,direct go install github.com/tal-tech/go-zero/tools/goctl@latest
|
||||
```
|
||||
|
||||
确保 goctl 可执行
|
||||
|
||||
2. 快速生成 api 服务
|
||||
|
||||
```shell
|
||||
goctl api new greet
|
||||
cd greet
|
||||
go mod init
|
||||
go mod tidy
|
||||
go run greet.go -f etc/greet-api.yaml
|
||||
```
|
||||
|
||||
默认侦听在 8888 端口(可以在配置文件里修改),可以通过 curl 请求:
|
||||
|
||||
```shell
|
||||
curl -i http://localhost:8888/from/you
|
||||
```
|
||||
|
||||
返回如下:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
Date: Thu, 22 Oct 2020 14:03:18 GMT
|
||||
Content-Length: 14
|
||||
|
||||
{"message":""}
|
||||
```
|
||||
|
||||
编写业务代码:
|
||||
|
||||
* api 文件定义了服务对外暴露的路由,可参考 [api 规范](https://github.com/tal-tech/zero-doc/blob/main/doc/goctl.md)
|
||||
* 可以在 servicecontext.go 里面传递依赖给 logic,比如 mysql, redis 等
|
||||
* 在 api 定义的 get/post/put/delete 等请求对应的 logic 里增加业务处理逻辑
|
||||
|
||||
3. 可以根据 api 文件生成前端需要的 Java, TypeScript, Dart, JavaScript 代码
|
||||
|
||||
```shell
|
||||
goctl api java -api greet.api -dir greet
|
||||
goctl api dart -api greet.api -dir greet
|
||||
...
|
||||
```
|
||||
|
||||
## 6. Benchmark
|
||||
|
||||
![benchmark](https://gitee.com/kevwan/static/raw/master/doc/images/benchmark.png)
|
||||
|
||||
[测试代码见这里](https://github.com/smallnest/go-web-framework-benchmark)
|
||||
|
||||
## 7. 文档
|
||||
|
||||
* [API 文档](api-grammar.md)
|
||||
|
||||
* [goctl 使用帮助](goctl.md)
|
||||
|
||||
* 常见问题
|
||||
|
||||
* 因为 `etcd` 和 `grpc` 兼容性问题,请使用 `grpc@v1.29.1`
|
||||
|
||||
`google.golang.org/grpc v1.29.1`
|
||||
|
||||
* 因为 `protobuf` 兼容性问题,请使用 `protocol-gen@v1.3.2`
|
||||
|
||||
`go get -u github.com/golang/protobuf/protoc-gen-go@v1.3.2`
|
||||
|
||||
* awesome 系列(更多文章见『微服务实践』公众号)
|
||||
* [快速构建高并发微服务](https://github.com/tal-tech/zero-doc/blob/main/doc/shorturl.md)
|
||||
* [快速构建高并发微服务 - 多 RPC 版](https://github.com/tal-tech/zero-doc/blob/main/docs/zero/bookstore.md)
|
||||
|
||||
* 精选 `goctl` 插件
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td>插件 </td> <td>用途 </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/zeromicro/goctl-swagger">goctl-swagger</a></td> <td>一键生成 <code>api</code> 的 <code>swagger</code> 文档 </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/zeromicro/goctl-android">goctl-android</a></td> <td> 生成 <code>java (android)</code> 端 <code>http client</code> 请求代码</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/zeromicro/goctl-go-compact">goctl-go-compact</a> </td> <td>合并 <code>api</code> 里同一个 <code>group</code> 里的 <code>handler</code> 到一个 go 文件</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## 8. 微信公众号
|
||||
|
||||
`go-zero` 相关文章都会在 `微服务实践` 公众号整理呈现,欢迎扫码关注,也可以通过公众号私信我 👏
|
||||
|
||||
<img src="https://zeromicro.github.io/go-zero-pages/resource/go-zero-practise.png" alt="wechat" width="300" />
|
||||
|
||||
## 9. 微信交流群
|
||||
|
||||
如果文档中未能覆盖的任何疑问,欢迎您在群里提出,我们会尽快答复。
|
||||
|
||||
您可以在群内提出使用中需要改进的地方,我们会考虑合理性并尽快修改。
|
||||
|
||||
如果您发现 ***bug*** 请及时提 ***issue***,我们会尽快确认并修改。
|
||||
|
||||
为了防止广告用户、识别技术同行,请 ***star*** 后加我时注明 **github** 当前 ***star*** 数,我再拉进 **go-zero** 群,感谢!
|
||||
|
||||
加我之前有劳点一下 ***star***,一个小小的 ***star*** 是作者们回答海量问题的动力🤝
|
||||
|
||||
<img src="https://raw.githubusercontent.com/tal-tech/zero-doc/main/doc/images/wechat.jpg" alt="wechat" width="300" />
|
20
go-zero.dev/cn/about-us.md
Normal file
20
go-zero.dev/cn/about-us.md
Normal file
@ -0,0 +1,20 @@
|
||||
# 关于我们
|
||||
|
||||
## go-zero
|
||||
go-zero 是一个集成了各种工程实践的 web 和 rpc 框架。通过弹性设计保障了大并发服务端的稳定性,经受了充分的实战检验。
|
||||
|
||||
go-zero 包含极简的 API 定义和生成工具 goctl,可以根据定义的 api 文件一键生成 Go, iOS, Android, Kotlin, Dart, TypeScript, JavaScript 代码,并可直接运行。
|
||||
|
||||
## go-zero作者
|
||||
[<img src="https://zeromicro.github.io/go-zero-pages/resource/author.jpeg" width="80px" height="80px" alt="kevwan"/>](https://github.com/kevwan)
|
||||
|
||||
**万俊峰**,晓黑板研发负责人,好未来技术委员会资深专家,拥有14年研发团队管理经验,16年架构设计经验,20年工程实战经验,负责过多个大型项目的架构设计,曾多次合伙创业(被收购),ArchSummit全球架构师峰会明星讲师,GopherChina大会金牌讲师,腾讯云开发者大会讲师。
|
||||
|
||||
## go-zero成员
|
||||
go-zero截止2021年4月,目前拥有30人的团队开发人员及60+的社区成员。
|
||||
|
||||
## go-zero社区
|
||||
我们目前拥有4000多人的社区成员,在这里,你可以和大家讨论任何关于go-zero的技术,问题反馈,获取最新的go-zero信息,以及各位大佬每天分享的技术心得。
|
||||
|
||||
## go-zero社区群
|
||||
<img src="https://raw.githubusercontent.com/tal-tech/zero-doc/main/doc/images/wechat.jpg" width="300" alt="社区群"/>
|
53
go-zero.dev/cn/api-coding.md
Normal file
53
go-zero.dev/cn/api-coding.md
Normal file
@ -0,0 +1,53 @@
|
||||
# api文件编写
|
||||
|
||||
## 编写user.api文件
|
||||
```shell
|
||||
$ vim service/user/cmd/api/user.api
|
||||
```
|
||||
```text
|
||||
type (
|
||||
LoginReq {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
LoginReply {
|
||||
Id int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Gender string `json:"gender"`
|
||||
AccessToken string `json:"accessToken"`
|
||||
AccessExpire int64 `json:"accessExpire"`
|
||||
RefreshAfter int64 `json:"refreshAfter"`
|
||||
}
|
||||
)
|
||||
|
||||
service user-api {
|
||||
@handler login
|
||||
post /user/login (LoginReq) returns (LoginReply)
|
||||
}
|
||||
```
|
||||
## 生成api服务
|
||||
### 方式一
|
||||
|
||||
```shell
|
||||
$ cd book/service/user/cmd/api
|
||||
$ goctl api go -api user.api -dir .
|
||||
```
|
||||
```text
|
||||
Done.
|
||||
```
|
||||
|
||||
### 方式二
|
||||
|
||||
在 `user.api` 文件右键,依次点击进入 `New`->`Go Zero`->`Api Code` ,进入目标目录选择,即api源码的目标存放目录,默认为user.api所在目录,选择好目录后点击OK即可。
|
||||
![api生成](https://zeromicro.github.io/go-zero-pages/resource/goctl-api.png)
|
||||
![api生成目录选择](https://zeromicro.github.io/go-zero-pages/resource/goctl-api-select.png)
|
||||
|
||||
### 方式三
|
||||
|
||||
打开user.api,进入编辑区,使用快捷键`Command+N`(for mac OS)或者 `alt+insert`(for windows),选择`Api Code`,同样进入目录选择弹窗,选择好目录后点击OK即可。
|
||||
|
||||
# 猜你想看
|
||||
* [api语法](api-grammar.md)
|
||||
* [goctl api命令](goctl-api.md)
|
||||
* [api目录结构介绍](api-dir.md)
|
110
go-zero.dev/cn/api-config.md
Normal file
110
go-zero.dev/cn/api-config.md
Normal file
@ -0,0 +1,110 @@
|
||||
# api配置
|
||||
api配置控制着api服务中的各种功能,包含但不限于服务监听地址,端口,环境配置,日志配置等,下面我们从一个简单的配置来看一下api中常用配置分别有什么作用。
|
||||
|
||||
## 配置说明
|
||||
通过yaml配置我们会发现,有很多参数我们并没有于config对齐,这是因为config定义中,有很多都是带`optional`或者`default`
|
||||
标签的,对于`optional`可选项,你可以根据自己需求判断是否需要设置,对于`default`标签,如果你觉得默认值就已经够了,可以不用设置,
|
||||
一般`default`中的值基本不用修改,可以认为是最佳实践值。
|
||||
|
||||
### Config
|
||||
|
||||
```go
|
||||
type Config struct{
|
||||
rest.RestConf // rest api配置
|
||||
Auth struct { // jwt鉴权配置
|
||||
AccessSecret string // jwt密钥
|
||||
AccessExpire int64 // 有效期,单位:秒
|
||||
}
|
||||
Mysql struct { // 数据库配置,除mysql外,可能还有mongo等其他数据库
|
||||
DataSource string // mysql链接地址,满足 $user:$password@tcp($ip:$port)/$db?$queries 格式即可
|
||||
}
|
||||
CacheRedis cache.CacheConf // redis缓存
|
||||
UserRpc zrpc.RpcClientConf // rpc client配置
|
||||
}
|
||||
```
|
||||
|
||||
### rest.RestConf
|
||||
api服务基础配置,包含监听地址,监听端口,证书配置,限流,熔断参数,超时参数等控制,对其展开我们可以看到:
|
||||
```go
|
||||
service.ServiceConf // service配置
|
||||
Host string `json:",default=0.0.0.0"` // http监听ip,默认0.0.0.0
|
||||
Port int // http监听端口,必填
|
||||
CertFile string `json:",optional"` // https证书文件,可选
|
||||
KeyFile string `json:",optional"` // https私钥文件,可选
|
||||
Verbose bool `json:",optional"` // 是否打印详细http请求日志
|
||||
MaxConns int `json:",default=10000"` // http同时可接受最大请求数(限流数),默认10000
|
||||
MaxBytes int64 `json:",default=1048576,range=[0:8388608]"` // http可接受请求的最大ContentLength,默认1048576,被设置值不能必须在0到8388608之间
|
||||
// milliseconds
|
||||
Timeout int64 `json:",default=3000"` // 超时时长控制,单位:毫秒,默认3000
|
||||
CpuThreshold int64 `json:",default=900,range=[0:1000]"` // cpu降载阈值,默认900,可允许设置范围0到1000
|
||||
Signature SignatureConf `json:",optional"` // 签名配置
|
||||
```
|
||||
|
||||
### service.ServiceConf
|
||||
```go
|
||||
type ServiceConf struct {
|
||||
Name string // 服务名称
|
||||
Log logx.LogConf // 日志配置
|
||||
Mode string `json:",default=pro,options=dev|test|pre|pro"` // 服务环境,dev-开发环境,test-测试环境,pre-预发环境,pro-正式环境
|
||||
MetricsUrl string `json:",optional"` // 指标上报接口地址,该地址需要支持post json即可
|
||||
Prometheus prometheus.Config `json:",optional"` // prometheus配置
|
||||
}
|
||||
```
|
||||
|
||||
### logx.LogConf
|
||||
```go
|
||||
type LogConf struct {
|
||||
ServiceName string `json:",optional"` // 服务名称
|
||||
Mode string `json:",default=console,options=console|file|volume"` // 日志模式,console-输出到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密码
|
||||
}
|
||||
```
|
24
go-zero.dev/cn/api-dir.md
Normal file
24
go-zero.dev/cn/api-dir.md
Normal file
@ -0,0 +1,24 @@
|
||||
# api目录介绍
|
||||
|
||||
```text
|
||||
.
|
||||
├── etc
|
||||
│ └── greet-api.yaml // 配置文件
|
||||
├── go.mod // mod文件
|
||||
├── greet.api // api描述文件
|
||||
├── greet.go // main函数入口
|
||||
└── internal
|
||||
├── config
|
||||
│ └── config.go // 配置声明type
|
||||
├── handler // 路由及handler转发
|
||||
│ ├── greethandler.go
|
||||
│ └── routes.go
|
||||
├── logic // 业务逻辑
|
||||
│ └── greetlogic.go
|
||||
├── middleware // 中间件文件
|
||||
│ └── greetmiddleware.go
|
||||
├── svc // logic所依赖的资源池
|
||||
│ └── servicecontext.go
|
||||
└── types // request、response的struct,根据api自动生成,不建议编辑
|
||||
└── types.go
|
||||
```
|
733
go-zero.dev/cn/api-grammar.md
Normal file
733
go-zero.dev/cn/api-grammar.md
Normal file
@ -0,0 +1,733 @@
|
||||
# api语法介绍
|
||||
|
||||
## api示例
|
||||
|
||||
```go
|
||||
/**
|
||||
* api语法示例及语法说明
|
||||
*/
|
||||
|
||||
// api语法版本
|
||||
syntax = "v1"
|
||||
|
||||
// import literal
|
||||
import "foo.api"
|
||||
|
||||
// import group
|
||||
import (
|
||||
"bar.api"
|
||||
"foo/bar.api"
|
||||
)
|
||||
info(
|
||||
author: "songmeizi"
|
||||
date: "2020-01-08"
|
||||
desc: "api语法示例及语法说明"
|
||||
)
|
||||
|
||||
// type literal
|
||||
|
||||
type Foo{
|
||||
Foo int `json:"foo"`
|
||||
}
|
||||
|
||||
// type group
|
||||
|
||||
type(
|
||||
Bar{
|
||||
Bar int `json:"bar"`
|
||||
}
|
||||
)
|
||||
|
||||
// service block
|
||||
@server(
|
||||
jwt: Auth
|
||||
group: foo
|
||||
)
|
||||
service foo-api{
|
||||
@doc "foo"
|
||||
@handler foo
|
||||
post /foo (Foo) returns (Bar)
|
||||
}
|
||||
```
|
||||
|
||||
## api语法结构
|
||||
|
||||
* syntax语法声明
|
||||
* import语法块
|
||||
* info语法块
|
||||
* type语法块
|
||||
* service语法块
|
||||
* 隐藏通道
|
||||
|
||||
> [!TIP]
|
||||
> 在以上语法结构中,各个语法块从语法上来说,按照语法块为单位,可以在.api文件中任意位置声明,
|
||||
> 但是为了提高阅读效率,我们建议按照以上顺序进行声明,因为在将来可能会通过严格模式来控制语法块的顺序。
|
||||
|
||||
### syntax语法声明
|
||||
|
||||
syntax是新加入的语法结构,该语法的引入可以解决:
|
||||
|
||||
* 快速针对api版本定位存在问题的语法结构
|
||||
* 针对版本做语法解析
|
||||
* 防止api语法大版本升级导致前后不能向前兼容
|
||||
|
||||
> **[!WARNING]
|
||||
> 被import的api必须要和main api的syntax版本一致。
|
||||
|
||||
**语法定义**
|
||||
|
||||
```antlrv4
|
||||
'syntax'={checkVersion(p)}STRING
|
||||
```
|
||||
|
||||
**语法说明**
|
||||
|
||||
syntax:固定token,标志一个syntax语法结构的开始
|
||||
|
||||
checkVersion:自定义go方法,检测`STRING`是否为一个合法的版本号,目前检测逻辑为,STRING必须是满足`(?m)"v[1-9][0-9]*"`正则。
|
||||
|
||||
STRING:一串英文双引号包裹的字符串,如"v1"
|
||||
|
||||
一个api语法文件只能有0或者1个syntax语法声明,如果没有syntax,则默认为v1版本
|
||||
|
||||
**正确语法示例** ✅
|
||||
|
||||
eg1:不规范写法
|
||||
|
||||
```api
|
||||
syntax="v1"
|
||||
```
|
||||
|
||||
eg2:规范写法(推荐)
|
||||
|
||||
```api
|
||||
syntax = "v2"
|
||||
```
|
||||
|
||||
**错误语法示例** ❌
|
||||
|
||||
eg1:
|
||||
|
||||
```api
|
||||
syntax = "v0"
|
||||
```
|
||||
|
||||
eg2:
|
||||
|
||||
```api
|
||||
syntax = v1
|
||||
```
|
||||
|
||||
eg3:
|
||||
|
||||
```api
|
||||
syntax = "V1"
|
||||
```
|
||||
|
||||
## import语法块
|
||||
|
||||
随着业务规模增大,api中定义的结构体和服务越来越多,所有的语法描述均为一个api文件,这是多么糟糕的一个问题, 其会大大增加了阅读难度和维护难度,import语法块可以帮助我们解决这个问题,通过拆分api文件,
|
||||
不同的api文件按照一定规则声明,可以降低阅读难度和维护难度。
|
||||
|
||||
> **[!WARNING]
|
||||
> 这里import不像golang那样包含package声明,仅仅是一个文件路径的引入,最终解析后会把所有的声明都汇聚到一个spec.Spec中。
|
||||
> 不能import多个相同路径,否则会解析错误。
|
||||
|
||||
**语法定义**
|
||||
|
||||
```antlrv4
|
||||
'import' {checkImportValue(p)}STRING
|
||||
|'import' '(' ({checkImportValue(p)}STRING)+ ')'
|
||||
```
|
||||
|
||||
**语法说明**
|
||||
|
||||
import:固定token,标志一个import语法的开始
|
||||
|
||||
checkImportValue:自定义go方法,检测`STRING`是否为一个合法的文件路径,目前检测逻辑为,STRING必须是满足`(?m)"(/?[a-zA-Z0-9_#-])+\.api"`正则。
|
||||
|
||||
STRING:一串英文双引号包裹的字符串,如"foo.api"
|
||||
|
||||
**正确语法示例** ✅
|
||||
|
||||
eg:
|
||||
|
||||
```api
|
||||
import "foo.api"
|
||||
import "foo/bar.api"
|
||||
|
||||
import(
|
||||
"bar.api"
|
||||
"foo/bar/foo.api"
|
||||
)
|
||||
```
|
||||
|
||||
**错误语法示例** ❌
|
||||
|
||||
eg:
|
||||
|
||||
```api
|
||||
import foo.api
|
||||
import "foo.txt"
|
||||
import (
|
||||
bar.api
|
||||
bar.api
|
||||
)
|
||||
```
|
||||
|
||||
## info语法块
|
||||
|
||||
info语法块是一个包含了多个键值对的语法体,其作用相当于一个api服务的描述,解析器会将其映射到spec.Spec中, 以备用于翻译成其他语言(golang、java等)
|
||||
时需要携带的meta元素。如果仅仅是对当前api的一个说明,而不考虑其翻译 时传递到其他语言,则使用简单的多行注释或者java风格的文档注释即可,关于注释说明请参考下文的 **隐藏通道**。
|
||||
|
||||
> **[!WARNING]
|
||||
> 不能使用重复的key,每个api文件只能有0或者1个info语法块
|
||||
|
||||
**语法定义**
|
||||
|
||||
```antlrv4
|
||||
'info' '(' (ID {checkKeyValue(p)}VALUE)+ ')'
|
||||
```
|
||||
|
||||
**语法说明**
|
||||
|
||||
info:固定token,标志一个info语法块的开始
|
||||
|
||||
checkKeyValue:自定义go方法,检测`VALUE`是否为一个合法值。
|
||||
|
||||
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/tal-tech/go-zero/blob/master/tools/goctl/api/parser/g4/ApiParser.g4) 中查看typeSpec定义。
|
||||
|
||||
**语法说明**
|
||||
|
||||
参考golang写法
|
||||
|
||||
**正确语法示例** ✅
|
||||
|
||||
eg1:不规范写法
|
||||
|
||||
```api
|
||||
type Foo struct{
|
||||
Id int `path:"id"` // ①
|
||||
Foo int `json:"foo"`
|
||||
}
|
||||
|
||||
type Bar struct{
|
||||
// 非导出型字段
|
||||
bar int `form:"bar"`
|
||||
}
|
||||
|
||||
type(
|
||||
// 非导出型结构体
|
||||
fooBar struct{
|
||||
FooBar int
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
eg2:规范写法(推荐)
|
||||
|
||||
```api
|
||||
type Foo{
|
||||
Id int `path:"id"`
|
||||
Foo int `json:"foo"`
|
||||
}
|
||||
|
||||
type Bar{
|
||||
Bar int `form:"bar"`
|
||||
}
|
||||
|
||||
type(
|
||||
FooBar{
|
||||
FooBar int
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
**错误语法示例** ❌
|
||||
|
||||
eg
|
||||
|
||||
```api
|
||||
type Gender int // 不支持
|
||||
|
||||
// 非struct token
|
||||
type Foo structure{
|
||||
CreateTime time.Time // 不支持time.Time
|
||||
}
|
||||
|
||||
// golang关键字 var
|
||||
type var{}
|
||||
|
||||
type Foo{
|
||||
// golang关键字 interface
|
||||
Foo interface
|
||||
}
|
||||
|
||||
|
||||
type Foo{
|
||||
foo int
|
||||
// map key必须要golang内置数据类型
|
||||
m map[Bar]string
|
||||
}
|
||||
```
|
||||
|
||||
> [!NOTE] ①
|
||||
> tag定义和golang中json tag语法一样,除了json tag外,go-zero还提供了另外一些tag来实现对字段的描述,
|
||||
> 详情见下表。
|
||||
|
||||
* tag表
|
||||
<table>
|
||||
<tr>
|
||||
<td>tag key</td> <td>描述</td> <td>提供方</td><td>有效范围 </td> <td>示例 </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>json</td> <td>json序列化tag</td> <td>golang</td> <td>request、response</td> <td><code>json:"fooo"</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>path</td> <td>路由path,如<code>/foo/:id</code></td> <td>go-zero</td> <td>request</td> <td><code>path:"id"</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>form</td> <td>标志请求体是一个form(POST方法时)或者一个query(GET方法时<code>/search?name=keyword</code>)</td> <td>go-zero</td> <td>request</td> <td><code>form:"name"</code></td>
|
||||
</tr>
|
||||
</table>
|
||||
* tag修饰符
|
||||
|
||||
常见参数校验描述
|
||||
<table>
|
||||
<tr>
|
||||
<td>tag key </td> <td>描述 </td> <td>提供方 </td> <td>有效范围 </td> <td>示例 </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>optional</td> <td>定义当前字段为可选参数</td> <td>go-zero</td> <td>request</td> <td><code>json:"name,optional"</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>options</td> <td>定义当前字段的枚举值,多个以竖线|隔开</td> <td>go-zero</td> <td>request</td> <td><code>json:"gender,options=male"</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>default</td> <td>定义当前字段默认值</td> <td>go-zero</td> <td>request</td> <td><code>json:"gender,default=male"</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>range</td> <td>定义当前字段数值范围</td> <td>go-zero</td> <td>request</td> <td><code>json:"age,range=[0:120]"</code></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
> [!TIP]
|
||||
> tag修饰符需要在tag value后以引文逗号,隔开
|
||||
|
||||
## service语法块
|
||||
|
||||
service语法块用于定义api服务,包含服务名称,服务metadata,中间件声明,路由,handler等。
|
||||
|
||||
> **[!WARNING]️
|
||||
> * main api和被import的api服务名称必须一致,不能出现服务名称歧义。
|
||||
> * handler名称不能重复
|
||||
> * 路由(请求方法+请求path)名称不能重复
|
||||
> * 请求体必须声明为普通(非指针)struct,响应体做了一些向前兼容处理,详请见下文说明
|
||||
>
|
||||
|
||||
**语法定义**
|
||||
|
||||
```antlrv4
|
||||
serviceSpec: atServer? serviceApi;
|
||||
atServer: '@server' lp='(' kvLit+ rp=')';
|
||||
serviceApi: {match(p,"service")}serviceToken=ID serviceName lbrace='{' serviceRoute* rbrace='}';
|
||||
serviceRoute: atDoc? (atServer|atHandler) route;
|
||||
atDoc: '@doc' lp='('? ((kvLit+)|STRING) rp=')'?;
|
||||
atHandler: '@handler' ID;
|
||||
route: {checkHttpMethod(p)}httpMethod=ID path request=body? returnToken=ID? response=replybody?;
|
||||
body: lp='(' (ID)? rp=')';
|
||||
replybody: lp='(' dataType? rp=')';
|
||||
// kv
|
||||
kvLit: key=ID {checkKeyValue(p)}value=LINE_VALUE;
|
||||
|
||||
serviceName: (ID '-'?)+;
|
||||
path: (('/' (ID ('-' ID)*))|('/:' (ID ('-' ID)?)))+;
|
||||
```
|
||||
|
||||
**语法说明**
|
||||
|
||||
serviceSpec:包含了一个可选语法块`atServer`和`serviceApi`语法块,其遵循序列模式(编写service必须要按照顺序,否则会解析出错)
|
||||
|
||||
atServer: 可选语法块,定义key-value结构的server metadata,'@server'
|
||||
表示这一个server语法块的开始,其可以用于描述serviceApi或者route语法块,其用于描述不同语法块时有一些特殊关键key 需要值得注意,见 **atServer关键key描述说明**。
|
||||
|
||||
serviceApi:包含了1到多个`serviceRoute`语法块
|
||||
|
||||
serviceRoute:按照序列模式包含了`atDoc`,handler和`route`
|
||||
|
||||
atDoc:可选语法块,一个路由的key-value描述,其在解析后会传递到spec.Spec结构体,如果不关心传递到spec.Spec, 推荐用单行注释替代。
|
||||
|
||||
handler:是对路由的handler层描述,可以通过atServer指定`handler` key来指定handler名称, 也可以直接用atHandler语法块来定义handler名称
|
||||
|
||||
atHandler:'@handler' 固定token,后接一个遵循正则`[_a-zA-Z][a-zA-Z_-]*`)的值,用于声明一个handler名称
|
||||
|
||||
route:路由,有`httpMethod`、`path`、可选`request`、可选`response`组成,`httpMethod`是必须是小写。
|
||||
|
||||
body:api请求体语法定义,必须要由()包裹的可选的ID值
|
||||
|
||||
replyBody:api响应体语法定义,必须由()包裹的struct、~~array(向前兼容处理,后续可能会废弃,强烈推荐以struct包裹,不要直接用array作为响应体)~~
|
||||
|
||||
kvLit: 同info key-value
|
||||
|
||||
serviceName: 可以有多个'-'join的ID值
|
||||
|
||||
path:api请求路径,必须以'/'或者'/:'开头,切不能以'/'结尾,中间可包含ID或者多个以'-'join的ID字符串
|
||||
|
||||
**atServer关键key描述说明**
|
||||
|
||||
修饰service时
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td>key</td><td>描述</td><td>示例</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>jwt</td><td>声明当前service下所有路由需要jwt鉴权,且会自动生成包含jwt逻辑的代码</td><td><code>jwt: Auth</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>group</td><td>声明当前service或者路由文件分组</td><td><code>group: login</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>middleware</td><td>声明当前service需要开启中间件</td><td><code>middleware: AuthMiddleware</code></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
修饰route时
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td>key</td><td>描述</td><td>示例</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>handler</td><td>声明一个handler</td><td>-</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
**正确语法示例** ✅
|
||||
|
||||
eg1:不规范写法
|
||||
|
||||
```api
|
||||
@server(
|
||||
jwt: Auth
|
||||
group: foo
|
||||
middleware: AuthMiddleware
|
||||
)
|
||||
service foo-api{
|
||||
@doc(
|
||||
summary: foo
|
||||
)
|
||||
@server(
|
||||
handler: foo
|
||||
)
|
||||
// 非导出型body
|
||||
post /foo/:id (foo) returns (bar)
|
||||
|
||||
@doc "bar"
|
||||
@handler bar
|
||||
post /bar returns ([]int)// 不推荐数组作为响应体
|
||||
|
||||
@handler fooBar
|
||||
post /foo/bar (Foo) returns // 可以省略'returns'
|
||||
}
|
||||
```
|
||||
|
||||
eg2:规范写法(推荐)
|
||||
|
||||
```api
|
||||
@server(
|
||||
jwt: Auth
|
||||
group: foo
|
||||
middleware: AuthMiddleware
|
||||
)
|
||||
service foo-api{
|
||||
@doc "foo"
|
||||
@handler foo
|
||||
post /foo/:id (Foo) returns (Bar)
|
||||
}
|
||||
|
||||
service foo-api{
|
||||
@handler ping
|
||||
get /ping
|
||||
|
||||
@doc "foo"
|
||||
@handler bar
|
||||
post /bar/:id (Foo)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**错误语法示例** ❌
|
||||
|
||||
```api
|
||||
// 不支持空的server语法块
|
||||
@server(
|
||||
)
|
||||
// 不支持空的service语法块
|
||||
service foo-api{
|
||||
}
|
||||
|
||||
service foo-api{
|
||||
@doc kkkk // 简版doc必须用英文双引号引起来
|
||||
@handler foo
|
||||
post /foo
|
||||
|
||||
@handler foo // 重复的handler
|
||||
post /bar
|
||||
|
||||
@handler fooBar
|
||||
post /bar // 重复的路由
|
||||
|
||||
// @handler和@doc顺序错误
|
||||
@handler someHandler
|
||||
@doc "some doc"
|
||||
post /some/path
|
||||
|
||||
// handler缺失
|
||||
post /some/path/:id
|
||||
|
||||
@handler reqTest
|
||||
post /foo/req (*Foo) // 不支持除普通结构体外的其他数据类型作为请求体
|
||||
|
||||
@handler replyTest
|
||||
post /foo/reply returns (*Foo) // 不支持除普通结构体、数组(向前兼容,后续考虑废弃)外的其他数据类型作为响应体
|
||||
}
|
||||
```
|
||||
|
||||
## 隐藏通道
|
||||
|
||||
隐藏通道目前主要为空白符号、换行符号以及注释,这里我们只说注释,因为空白符号和换行符号我们目前拿来也无用。
|
||||
|
||||
### 单行注释
|
||||
|
||||
**语法定义**
|
||||
|
||||
```antlrv4
|
||||
'//' ~[\r\n]*
|
||||
```
|
||||
|
||||
**语法说明**
|
||||
由语法定义可知道,单行注释必须要以`//`开头,内容为不能包含换行符
|
||||
|
||||
**正确语法示例** ✅
|
||||
|
||||
```api
|
||||
// doc
|
||||
// comment
|
||||
```
|
||||
|
||||
**错误语法示例** ❌
|
||||
|
||||
```api
|
||||
// break
|
||||
line comments
|
||||
```
|
||||
|
||||
### java风格文档注释
|
||||
|
||||
**语法定义**
|
||||
|
||||
```antlrv4
|
||||
'/*' .*? '*/'
|
||||
```
|
||||
|
||||
**语法说明**
|
||||
|
||||
由语法定义可知道,单行注释必须要以`/*`开头,`*/`结尾的任意字符。
|
||||
|
||||
**正确语法示例** ✅
|
||||
|
||||
```api
|
||||
/**
|
||||
* java-style doc
|
||||
*/
|
||||
```
|
||||
|
||||
**错误语法示例** ❌
|
||||
|
||||
```api
|
||||
/*
|
||||
* java-style doc */
|
||||
*/
|
||||
```
|
||||
|
||||
## Doc&Comment
|
||||
|
||||
如果想获取某一个元素的doc或者comment开发人员需要怎么定义?
|
||||
|
||||
**Doc**
|
||||
|
||||
我们规定上一个语法块(非隐藏通道内容)的行数line+1到当前语法块第一个元素前的所有注释(当行,或者多行)均为doc, 且保留了`//`、`/*`、`*/`原始标记。
|
||||
|
||||
**Comment**
|
||||
|
||||
我们规定当前语法块最后一个元素所在行开始的一个注释块(当行,或者多行)为comment 且保留了`//`、`/*`、`*/`原始标记。
|
||||
语法块Doc和Comment的支持情况
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td>语法块</td><td>parent语法块</td><td>Doc</td><td>Comment</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>syntaxLit</td><td>api</td><td>✅</td><td>✅</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>kvLit</td><td>infoSpec</td><td>✅</td><td>✅</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>importLit</td><td>importSpec</td><td>✅</td><td>✅</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>typeLit</td><td>api</td><td>✅</td><td>❌</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>typeLit</td><td>typeBlock</td><td>✅</td><td>❌</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>field</td><td>typeLit</td><td>✅</td><td>✅</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>key-value</td><td>atServer</td><td>✅</td><td>✅</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>atHandler</td><td>serviceRoute</td><td>✅</td><td>✅</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>route</td><td>serviceRoute</td><td>✅</td><td>✅</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
以下为对应语法块解析后细带doc和comment的写法
|
||||
|
||||
```api
|
||||
// syntaxLit doc
|
||||
syntax = "v1" // syntaxLit commnet
|
||||
|
||||
info(
|
||||
// kvLit doc
|
||||
author: songmeizi // kvLit comment
|
||||
)
|
||||
|
||||
// typeLit doc
|
||||
type Foo {}
|
||||
|
||||
type(
|
||||
// typeLit doc
|
||||
Bar{}
|
||||
|
||||
FooBar{
|
||||
// filed doc
|
||||
Name int // filed comment
|
||||
}
|
||||
)
|
||||
|
||||
@server(
|
||||
/**
|
||||
* kvLit doc
|
||||
* 开启jwt鉴权
|
||||
*/
|
||||
jwt: Auth /**kvLit comment*/
|
||||
)
|
||||
service foo-api{
|
||||
// atHandler doc
|
||||
@handler foo //atHandler comment
|
||||
|
||||
/*
|
||||
* route doc
|
||||
* post请求
|
||||
* path为 /foo
|
||||
* 请求体:Foo
|
||||
* 响应体:Foo
|
||||
*/
|
||||
post /foo (Foo) returns (Foo) // route comment
|
||||
}
|
||||
```
|
77
go-zero.dev/cn/bloom.md
Normal file
77
go-zero.dev/cn/bloom.md
Normal file
@ -0,0 +1,77 @@
|
||||
# bloom
|
||||
|
||||
go-zero微服务框架中提供了许多开箱即用的工具,好的工具不仅能提升服务的性能而且还能提升代码的鲁棒性避免出错,实现代码风格的统一方便他人阅读等等,本系列文章将分别介绍go-zero框架中工具的使用及其实现原理
|
||||
|
||||
## 布隆过滤器[bloom](https://github.com/zeromicro/go-zero/blob/master/core/bloom/bloom.go)
|
||||
在做服务器开发的时候,相信大家有听过布隆过滤器,可以判断某元素在不在集合里面,因为存在一定的误判和删除复杂问题,一般的使用场景是:防止缓存击穿(防止恶意攻击)、 垃圾邮箱过滤、cache digests 、模型检测器等、判断是否存在某行数据,用以减少对磁盘访问,提高服务的访问性能。 go-zero 提供的简单的缓存封装 bloom.bloom,简单使用方式如下
|
||||
|
||||
```go
|
||||
// 初始化 redisBitSet
|
||||
store := redis.NewRedis("redis 地址", redis.NodeType)
|
||||
// 声明一个bitSet, key="test_key"名且bits是1024位
|
||||
bitSet := newRedisBitSet(store, "test_key", 1024)
|
||||
// 判断第0位bit存不存在
|
||||
isSetBefore, err := bitSet.check([]uint{0})
|
||||
|
||||
// 对第512位设置为1
|
||||
err = bitSet.set([]uint{512})
|
||||
// 3600秒后过期
|
||||
err = bitSet.expire(3600)
|
||||
|
||||
// 删除该bitSet
|
||||
err = bitSet.del()
|
||||
```
|
||||
|
||||
|
||||
bloom 简单介绍了最基本的redis bitset 的使用。下面是真正的bloom实现。
|
||||
对元素hash 定位
|
||||
|
||||
```go
|
||||
// 对元素进行hash 14次(const maps=14),每次都在元素后追加byte(0-13),然后进行hash.
|
||||
// 将locations[0-13] 进行取模,最终返回locations.
|
||||
func (f *BloomFilter) getLocations(data []byte) []uint {
|
||||
locations := make([]uint, maps)
|
||||
for i := uint(0); i < maps; i++ {
|
||||
hashValue := hash.Hash(append(data, byte(i)))
|
||||
locations[i] = uint(hashValue % uint64(f.bits))
|
||||
}
|
||||
|
||||
return locations
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
向bloom里面add 元素
|
||||
```go
|
||||
// 我们可以发现 add方法使用了getLocations和bitSet的set方法。
|
||||
// 我们将元素进行hash成长度14的uint切片,然后进行set操作存到redis的bitSet里面。
|
||||
func (f *BloomFilter) Add(data []byte) error {
|
||||
locations := f.getLocations(data)
|
||||
err := f.bitSet.set(locations)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
检查bloom里面是否有某元素
|
||||
```go
|
||||
// 我们可以发现 Exists方法使用了getLocations和bitSet的check方法
|
||||
// 我们将元素进行hash成长度14的uint切片,然后进行bitSet的check验证,存在返回true,不存在或者check失败返回false
|
||||
func (f *BloomFilter) Exists(data []byte) (bool, error) {
|
||||
locations := f.getLocations(data)
|
||||
isSet, err := f.bitSet.check(locations)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !isSet {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
```
|
||||
|
||||
本节主要介绍了go-zero框架中的 core.bloom 工具,在实际的项目中非常实用。用好工具对于提升服务性能和开发效率都有很大的帮助,希望本篇文章能给大家带来一些收获。
|
@ -2,7 +2,7 @@
|
||||
|
||||
在微服务中服务间依赖非常常见,比如评论服务依赖审核服务而审核服务又依赖反垃圾服务,当评论服务调用审核服务时,审核服务又调用反垃圾服务,而这时反垃圾服务超时了,由于审核服务依赖反垃圾服务,反垃圾服务超时导致审核服务逻辑一直等待,而这个时候评论服务又在一直调用审核服务,审核服务就有可能因为堆积了大量请求而导致服务宕机
|
||||
|
||||
<img src="./images/call_chain.png" alt="call_chain" style="zoom:60%;" />
|
||||
![call_chain](./resource/call_chain.png)
|
||||
|
||||
由此可见,在整个调用链中,中间的某一个环节出现异常就会引起上游调用服务出现一些列的问题,甚至导致整个调用链的服务都宕机,这是非常可怕的。因此一个服务作为调用方调用另一个服务时,为了防止被调用服务出现问题进而导致调用服务出现问题,所以调用服务需要进行自我保护,而保护的常用手段就是***熔断***
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
- 打开(Open):在该状态下,发起请求时会立即返回错误,一般会启动一个超时计时器,当计时器超时后,状态切换到半打开状态,也可以设置一个定时器,定期的探测服务是否恢复
|
||||
- 半打开(Half-Open):在该状态下,允许应用程序一定数量的请求发往被调用服务,如果这些调用正常,那么可以认为被调用服务已经恢复正常,此时熔断器切换到关闭状态,同时需要重置计数。如果这部分仍有调用失败的情况,则认为被调用方仍然没有恢复,熔断器会切换到关闭状态,然后重置计数器,半打开状态能够有效防止正在恢复中的服务被突然大量请求再次打垮
|
||||
|
||||
<img src="./images/breaker_state.png" alt="breaker_state" style="zoom:50%;" />
|
||||
![breaker_state](./resource/breaker_state.png)
|
||||
|
||||
服务治理中引入熔断机制,使得系统更加稳定和有弹性,在系统从错误中恢复的时候提供稳定性,并且减少了错误对系统性能的影响,可以快速拒绝可能导致错误的服务调用,而不需要等待真正的错误返回
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
|
||||
我们知道,熔断器主要是用来保护调用端,调用端在发起请求的时候需要先经过熔断器,而客户端拦截器正好兼具了这个这个功能,所以在zRPC框架内熔断器是实现在客户端拦截器内,拦截器的原理如下图:
|
||||
|
||||
<img src="./images/interceptor.png" alt="interceptor" style="zoom:50%;" />
|
||||
![interceptor](./resource/interceptor.png)
|
||||
|
||||
对应的代码为:
|
||||
|
||||
@ -52,7 +52,7 @@ zRPC中熔断器的实现参考了[Google Sre过载保护算法](https://landing
|
||||
|
||||
在正常情况下,这两个值是相等的,随着被调用方服务出现异常开始拒绝请求,请求接受数量(accepts)的值开始逐渐小于请求数量(requests),这个时候调用方可以继续发送请求,直到requests = K * accepts,一旦超过这个限制,熔断器就回打开,新的请求会在本地以一定的概率被抛弃直接返回错误,概率的计算公式如下:
|
||||
|
||||
<img src="./images/client_rejection2.png" alt="client_rejection2" style="zoom:30%;" />
|
||||
![client_rejection2](./resource/client_rejection2.png)
|
||||
|
||||
通过修改算法中的K(倍值),可以调节熔断器的敏感度,当降低该倍值会使自适应熔断算法更敏感,当增加该倍值会使得自适应熔断算法降低敏感度,举例来说,假设将调用方的请求上限从 requests = 2 * acceptst 调整为 requests = 1.1 * accepts 那么就意味着调用方每十个请求之中就有一个请求会触发熔断
|
||||
|
125
go-zero.dev/cn/buiness-cache.md
Normal file
125
go-zero.dev/cn/buiness-cache.md
Normal file
@ -0,0 +1,125 @@
|
||||
# go-zero缓存设计之业务层缓存
|
||||
|
||||
在上一篇[go-zero缓存设计之持久层缓存](redis-cache.md)介绍了db层缓存,回顾一下,db层缓存主要设计可以总结为:
|
||||
|
||||
* 缓存只删除不更新
|
||||
* 行记录始终只存储一份,即主键对应行记录
|
||||
* 唯一索引仅缓存主键值,不直接缓存行记录(参考mysql索引思想)
|
||||
* 防缓存穿透设计,默认一分钟
|
||||
* 不缓存多行记录
|
||||
|
||||
## 前言
|
||||
|
||||
在大型业务系统中,通过对持久层添加缓存,对于大多数单行记录查询,相信缓存能够帮持久层减轻很大的访问压力,但在实际业务中,数据读取不仅仅只是单行记录,
|
||||
面对大量多行记录的查询,这对持久层也会造成不小的访问压力,除此之外,像秒杀系统、选课系统这种高并发的场景,单纯靠持久层的缓存是不现实的,本节我们来 介绍go-zero实践中的缓存设计——biz缓存。
|
||||
|
||||
## 适用场景举例
|
||||
|
||||
* 选课系统
|
||||
* 内容社交系统
|
||||
* 秒杀 ...
|
||||
|
||||
像这些系统,我们可以在业务层再增加一层缓存来存储系统中的关键信息,如选课系统中学生选课信息,课程剩余名额;内容社交系统中某一段时间之间的内容信息等。
|
||||
|
||||
接下来,我们一内容社交系统来进行举例说明。
|
||||
|
||||
在内容社交系统中,我们一般是先查询一批内容列表,然后点击某条内容查看详情,
|
||||
|
||||
在没有添加biz缓存前,内容信息的查询流程图应该为:
|
||||
|
||||
![redis-cache-05](./resource/redis-cache-05.png)
|
||||
|
||||
从图以及上一篇文章[go-zero缓存设计之持久层缓存](redis-cache.md)中我们可以知道,内容列表的获取是没办法依赖缓存的,
|
||||
如果我们在业务层添加一层缓存用来存储列表中的关键信息(甚至完整信息),那么多行记录的访问不在是一个问题,这就是biz redis要做的事情。 接下来我们来看一下设计方案,假设内容系统中单行记录包含以下字段
|
||||
|
||||
|字段名称|字段类型|备注|
|
||||
|---|---|---|
|
||||
|id|string|内容id|
|
||||
|title|string|标题|
|
||||
|content|string|详细内容|
|
||||
|createTime|time.Time|创建时间|
|
||||
|
||||
我们的目标是获取一批内容列表,而尽量避免内容列表走db造成访问压力,首先我们采用redis的sort set数据结构来存储,根需要存储的字段信息量,有两种redis存储方案:
|
||||
|
||||
* 缓存局部信息
|
||||
|
||||
![biz-redis-02](./resource/biz-redis-02.svg)
|
||||
对其关键字段信息(如:id等)按照一定规则压缩,并存储,score我们用`createTime`毫秒值(时间值相等这里不讨论),这种存储方案的好处是节约redis存储空间,
|
||||
那另一方面,缺点就是需要对列表详细内容进行二次回查(但这次回查是会利用到持久层的行记录缓存的)
|
||||
|
||||
* 缓存完整信息
|
||||
|
||||
![biz-redis-01](./resource/biz-redis-01.svg)
|
||||
对发布的所有内容按照一定规则压缩后均进行存储,同样score我们还是用`createTime`毫秒值,这种存储方案的好处是业务的增、删、查、改均走reids,而db层这时候
|
||||
就可以不用考虑行记录缓存了,持久层仅提供数据备份和恢复使用,从另一方面来看,其缺点也很明显,需要的存储空间、配置要求更高,费用也会随之增大。
|
||||
|
||||
示例代码:
|
||||
```golang
|
||||
type Content struct {
|
||||
Id string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
CreateTime time.Time `json:"create_time"`
|
||||
}
|
||||
|
||||
const bizContentCacheKey = `biz#content#cache`
|
||||
|
||||
// AddContent 提供内容存储
|
||||
func AddContent(r redis.Redis, c *Content) error {
|
||||
v := compress(c)
|
||||
_, err := r.Zadd(bizContentCacheKey, c.CreateTime.UnixNano()/1e6, v)
|
||||
return err
|
||||
}
|
||||
|
||||
// DelContent 提供内容删除
|
||||
func DelContent(r redis.Redis, c *Content) error {
|
||||
v := compress(c)
|
||||
_, err := r.Zrem(bizContentCacheKey, v)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// 内容压缩
|
||||
func compress(c *Content) string {
|
||||
// todo: do it yourself
|
||||
var ret string
|
||||
return ret
|
||||
}
|
||||
|
||||
// 内容解压
|
||||
func unCompress(v string) *Content {
|
||||
// todo: do it yourself
|
||||
var ret Content
|
||||
return &ret
|
||||
}
|
||||
|
||||
// ListByRangeTime提供根据时间段进行数据查询
|
||||
func ListByRangeTime(r redis.Redis, start, end time.Time) ([]*Content, error) {
|
||||
kvs, err := r.ZrangebyscoreWithScores(bizContentCacheKey, start.UnixNano()/1e6, end.UnixNano()/1e6)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var list []*Content
|
||||
for _, kv := range kvs {
|
||||
data:=unCompress(kv.Key)
|
||||
list = append(list, data)
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在以上例子中,redis是没有设置过期时间的,我们将增、删、改、查操作均同步到redis,我们认为内容社交系统的列表访问请求是比较高的情况下才做这样的方案设计,
|
||||
除此之外,还有一些数据访问,没有想内容设计系统这么频繁的访问, 可能是某一时间段内访问量突如其来的增加,之后可能很长一段时间才会再访问一次,以此间隔,
|
||||
或者说不会再访问了,面对这种场景,如果我又该如何考虑缓存的设计呢?在go-zero内容实践中,有两种方案可以解决这种问题:
|
||||
|
||||
* 增加内存缓存:通过内存缓存来存储当前可能突发访问量比较大的数据,常用的存储方案采用map数据结构来存储,map数据存储实现比较简单,但缓存过期处理则需要增加
|
||||
定时器来出来,另一宗方案是通过go-zero库中的 [Cache](https://github.com/tal-tech/go-zero/blob/master/core/collection/cache.go) ,其是专门
|
||||
用于内存管理.
|
||||
* 采用biz redis,并设置合理的过期时间
|
||||
|
||||
# 总结
|
||||
以上两个场景可以包含大部分的多行记录缓存,对于多行记录查询量不大的场景,暂时没必要直接把biz redis放进去,可以先尝试让db来承担,开发人员可以根据持久层监控及服务
|
||||
监控来衡量时候需要引入biz。
|
124
go-zero.dev/cn/business-coding.md
Normal file
124
go-zero.dev/cn/business-coding.md
Normal file
@ -0,0 +1,124 @@
|
||||
# 业务编码
|
||||
前面一节,我们已经根据初步需求编写了user.api来描述user服务对外提供哪些服务访问,在本节我们接着前面的步伐,
|
||||
通过业务编码来讲述go-zero怎么在实际业务中使用。
|
||||
|
||||
## 添加Mysql配置
|
||||
```shell
|
||||
$ vim service/user/cmd/api/internal/config/config.go
|
||||
```
|
||||
```go
|
||||
package config
|
||||
|
||||
import "github.com/tal-tech/go-zero/rest"
|
||||
|
||||
type Config struct {
|
||||
rest.RestConf
|
||||
Mysql struct{
|
||||
DataSource string
|
||||
}
|
||||
|
||||
CacheRedis cache.CacheConf
|
||||
}
|
||||
```
|
||||
|
||||
## 完善yaml配置
|
||||
```shell
|
||||
$ vim service/user/cmd/api/etc/user-api.yaml
|
||||
```
|
||||
```yaml
|
||||
Name: user-api
|
||||
Host: 0.0.0.0
|
||||
Port: 8888
|
||||
Mysql:
|
||||
DataSource: $user:$password@tcp($url)/$db?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai
|
||||
CacheRedis:
|
||||
- Host: $host
|
||||
Pass: $pass
|
||||
Type: node
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> $user: mysql数据库user
|
||||
>
|
||||
> $password: mysql数据库密码
|
||||
>
|
||||
> $url: mysql数据库连接地址
|
||||
>
|
||||
> $db: mysql数据库db名称,即user表所在database
|
||||
>
|
||||
> $host: redis连接地址 格式:ip:port,如:127.0.0.1:6379
|
||||
>
|
||||
> $pass: redis密码
|
||||
>
|
||||
> 更多配置信息,请参考[api配置介绍](api-config.md)
|
||||
|
||||
## 完善服务依赖
|
||||
```shell
|
||||
$ vim service/user/cmd/api/internal/svc/servicecontext.go
|
||||
```
|
||||
```go
|
||||
type ServiceContext struct {
|
||||
Config config.Config
|
||||
UserModel model.UserModel
|
||||
}
|
||||
|
||||
func NewServiceContext(c config.Config) *ServiceContext {
|
||||
conn:=sqlx.NewMysql(c.Mysql.DataSource)
|
||||
return &ServiceContext{
|
||||
Config: c,
|
||||
UserModel: model.NewUserModel(conn,c.CacheRedis),
|
||||
}
|
||||
}
|
||||
```
|
||||
## 填充登录逻辑
|
||||
```shell
|
||||
$ vim service/user/cmd/api/internal/logic/loginlogic.go
|
||||
```
|
||||
|
||||
```go
|
||||
func (l *LoginLogic) Login(req types.LoginReq) (*types.LoginReply, error) {
|
||||
if len(strings.TrimSpace(req.Username)) == 0 || len(strings.TrimSpace(req.Password)) == 0 {
|
||||
return nil, errors.New("参数错误")
|
||||
}
|
||||
|
||||
userInfo, err := l.svcCtx.UserModel.FindOneByNumber(req.Username)
|
||||
switch err {
|
||||
case nil:
|
||||
case model.ErrNotFound:
|
||||
return nil, errors.New("用户名不存在")
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if userInfo.Password != req.Password {
|
||||
return nil, errors.New("用户密码不正确")
|
||||
}
|
||||
|
||||
// ---start---
|
||||
now := time.Now().Unix()
|
||||
accessExpire := l.svcCtx.Config.Auth.AccessExpire
|
||||
jwtToken, err := l.getJwtToken(l.svcCtx.Config.Auth.AccessSecret, now, l.svcCtx.Config.Auth.AccessExpire, userInfo.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// ---end---
|
||||
|
||||
return &types.LoginReply{
|
||||
Id: userInfo.Id,
|
||||
Name: userInfo.Name,
|
||||
Gender: userInfo.Gender,
|
||||
AccessToken: jwtToken,
|
||||
AccessExpire: now + accessExpire,
|
||||
RefreshAfter: now + accessExpire/2,
|
||||
}, nil
|
||||
}
|
||||
```
|
||||
> [!TIP]
|
||||
> 上述代码中 [start]-[end]的代码实现见[jwt鉴权](jwt.md)章节
|
||||
|
||||
# 猜你想看
|
||||
* [api语法](api-grammar.md)
|
||||
* [goctl api命令](goctl-api.md)
|
||||
* [api目录结构介绍](api-dir.md)
|
||||
* [jwt鉴权](jwt.md)
|
||||
* [api配置介绍](api-config.md)
|
59
go-zero.dev/cn/business-dev.md
Normal file
59
go-zero.dev/cn/business-dev.md
Normal file
@ -0,0 +1,59 @@
|
||||
# 业务开发
|
||||
本章节我们用一个简单的示例去演示一下go-zero中的一些基本功能。本节将包含以下小节:
|
||||
* [目录拆分](service-design.md)
|
||||
* [model生成](model-gen.md)
|
||||
* [api文件编写](api-coding.md)
|
||||
* [业务编码](business-coding.md)
|
||||
* [jwt鉴权](jwt.md)
|
||||
* [中间件使用](middleware.md)
|
||||
* [rpc服务编写与调用](rpc-call.md)
|
||||
* [错误处理](error-handle.md)
|
||||
|
||||
## 演示工程下载
|
||||
在正式进入后续文档叙述前,可以先留意一下这里的源码,后续我们会基于这份源码进行功能的递进式演示,
|
||||
而不是完全从0开始,如果你从[快速入门](quick-start.md)章节过来,这份源码结构对你来说不是问题。
|
||||
|
||||
点击<a href="https://zeromicro.github.io/go-zero-pages/resource/book.zip">这里下载</a>演示工程基础源码
|
||||
|
||||
## 演示工程说明
|
||||
|
||||
### 场景
|
||||
程序员小明需要借阅一本《西游记》,在没有线上图书管理系统的时候,他每天都要去图书馆前台咨询图书馆管理员,
|
||||
* 小明:你好,请问今天《西游记》的图书还有吗?
|
||||
* 管理员:没有了,明天再来看看吧。
|
||||
|
||||
过了一天,小明又来到图书馆,问:
|
||||
* 小明:你好,请问今天《西游记》的图书还有吗?
|
||||
* 管理员:没有了,你过两天再来看看吧。
|
||||
|
||||
就这样经过多次反复,小明也是徒劳无功,浪费大量时间在来回的路上,于是终于忍受不了落后的图书管理系统,
|
||||
他决定自己亲手做一个图书查阅系统。
|
||||
|
||||
### 预期实现目标
|
||||
* 用户登录
|
||||
依靠现有学生系统数据进行登录
|
||||
* 图书检索
|
||||
根据图书关键字搜索图书,查询图书剩余数量。
|
||||
|
||||
### 系统分析
|
||||
|
||||
#### 服务拆分
|
||||
* user
|
||||
* api 提供用户登录协议
|
||||
* rpc 供search服务访问用户数据
|
||||
* search
|
||||
* api 提供图书查询协议
|
||||
|
||||
> [!TIP]
|
||||
> 这个微小的图书借阅查询系统虽然小,从实际来讲不太符合业务场景,但是仅上面两个功能,已经满足我们对go-zero api/rpc的场景演示了,
|
||||
> 后续为了满足更丰富的go-zero功能演示,会在文档中进行业务插入即相关功能描述。这里仅用一个场景进行引入。
|
||||
>
|
||||
> 注意:user中的sql语句请自行创建到db中去,更多准备工作见[准备工作](prepare.md)
|
||||
>
|
||||
> 添加一些预设的用户数据到数据库,便于后面使用,为了篇幅,演示工程不对插入数据这种操作做详细演示。
|
||||
|
||||
|
||||
# 参考预设数据
|
||||
```sql
|
||||
INSERT INTO `user` (number,name,password,gender)values ('666','小明','123456','男');
|
||||
```
|
57
go-zero.dev/cn/ci-cd.md
Normal file
57
go-zero.dev/cn/ci-cd.md
Normal file
@ -0,0 +1,57 @@
|
||||
# CI/CD
|
||||
> 在软件工程中,CI/CD或CICD通常指的是持续集成和持续交付或持续部署的组合实践。
|
||||
> ——引自[维基百科](https://zh.wikipedia.org/wiki/CI/CD)
|
||||
|
||||
|
||||
![cd-cd](./resource/ci-cd.png)
|
||||
|
||||
## CI可以做什么?
|
||||
|
||||
> 现代应用开发的目标是让多位开发人员同时处理同一应用的不同功能。但是,如果企业安排在一天内将所有分支源代码合并在一起(称为“合并日”),最终可能造成工作繁琐、耗时,而且需要手动完成。这是因为当一位独立工作的开发人员对应用进行更改时,有可能会与其他开发人员同时进行的更改发生冲突。如果每个开发人员都自定义自己的本地集成开发环境(IDE),而不是让团队就一个基于云的 IDE 达成一致,那么就会让问题更加雪上加霜。
|
||||
|
||||
> 持续集成(CI)可以帮助开发人员更加频繁地(有时甚至每天)将代码更改合并到共享分支或“主干”中。一旦开发人员对应用所做的更改被合并,系统就会通过自动构建应用并运行不同级别的自动化测试(通常是单元测试和集成测试)来验证这些更改,确保这些更改没有对应用造成破坏。这意味着测试内容涵盖了从类和函数到构成整个应用的不同模块。如果自动化测试发现新代码和现有代码之间存在冲突,CI 可以更加轻松地快速修复这些错误。
|
||||
|
||||
> ——引自[《CI/CD是什么?如何理解持续集成、持续交付和持续部署》](https://www.redhat.com/zh/topics/devops/what-is-ci-cd)
|
||||
|
||||
从概念上来看,CI/CD包含部署过程,我们这里将部署(CD)单独放在一节[服务部署](service-deployment.md),
|
||||
本节就以gitlab来做简单的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/)
|
43
go-zero.dev/cn/coding-spec.md
Normal file
43
go-zero.dev/cn/coding-spec.md
Normal file
@ -0,0 +1,43 @@
|
||||
# 编码规范
|
||||
|
||||
## import
|
||||
* 单行import不建议用圆括号包裹
|
||||
* 按照`官方包`,NEW LINE,`当前工程包`,NEW LINE,`第三方依赖包`顺序引入
|
||||
```go
|
||||
import (
|
||||
"context"
|
||||
"string"
|
||||
|
||||
"greet/user/internal/config"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
```
|
||||
|
||||
## 函数返回
|
||||
* 对象避免非指针返回
|
||||
* 遵循有正常值返回则一定无error,有error则一定无正常值返回的原则
|
||||
|
||||
## 错误处理
|
||||
* 有error必须处理,如果不能处理就必须抛出。
|
||||
* 避免下划线(_)接收error
|
||||
|
||||
## 函数体编码
|
||||
* 建议一个block结束空一行,如if、for等
|
||||
```go
|
||||
func main (){
|
||||
if x==1{
|
||||
// do something
|
||||
}
|
||||
|
||||
fmt.println("xxx")
|
||||
}
|
||||
```
|
||||
* return前空一行
|
||||
```go
|
||||
func getUser(id string)(string,error){
|
||||
....
|
||||
|
||||
return "xx",nil
|
||||
}
|
||||
```
|
@ -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) {
|
41
go-zero.dev/cn/concept-introduction.md
Normal file
41
go-zero.dev/cn/concept-introduction.md
Normal file
@ -0,0 +1,41 @@
|
||||
# 概念介绍
|
||||
|
||||
## go-zero
|
||||
晓黑板golang开源项目,集各种工程实践于一身的web和rpc框架。
|
||||
|
||||
## goctl
|
||||
一个旨在为开发人员提高工程效率、降低出错率的辅助工具。
|
||||
|
||||
## goctl插件
|
||||
指以goctl为中心的周边二进制资源,能够满足一些个性化的代码生成需求,如路由合并插件`goctl-go-compact`插件,
|
||||
生成swagger文档的`goctl-swagger`插件,生成php调用端的`goctl-php`插件等。
|
||||
|
||||
## intellij/vscode插件
|
||||
在intellij系列产品上配合goctl开发的插件,其将goctl命令行操作使用UI进行替代。
|
||||
|
||||
## api文件
|
||||
api文件是指用于定义和描述api服务的文本文件,其以.api后缀结尾,包含api语法描述内容。
|
||||
|
||||
## goctl环境
|
||||
goctl环境是使用goctl前的准备环境,包含
|
||||
* golang环境
|
||||
* protoc
|
||||
* protoc-gen-go插件
|
||||
* go module | gopath
|
||||
|
||||
## go-zero-demo
|
||||
go-zero-demo里面包含了文档中所有源码的一个大仓库,后续我们在编写演示demo时,我们均在此项目下创建子项目,
|
||||
因此我们需要提前创建一个大仓库`go-zero-demo`,我这里把这个仓库放在home目录下。
|
||||
```shell
|
||||
$ cd ~
|
||||
$ mkdir go-zero-demo&&cd go-zero-demo
|
||||
$ go mod init go-zero-demo
|
||||
```
|
||||
|
||||
|
||||
# 参考文档
|
||||
* [go-zero](README.md)
|
||||
* [Goctl](goctl.md)
|
||||
* [插件中心](plugin-center.md)
|
||||
* [工具中心](tool-center.md)
|
||||
* [api语法](api-grammar.md)
|
4
go-zero.dev/cn/config-introduction.md
Normal file
4
go-zero.dev/cn/config-introduction.md
Normal file
@ -0,0 +1,4 @@
|
||||
# 配置介绍
|
||||
在正式使用go-zero之前,让我们先来了解一下go-zero中不同服务类型的配置定义,看看配置中每个字段分别有什么作用,本节将包含以下小节:
|
||||
* [api配置](api-config.md)
|
||||
* [rpc配置](rpc-config.md)
|
1053
go-zero.dev/cn/datacenter.md
Normal file
1053
go-zero.dev/cn/datacenter.md
Normal file
File diff suppressed because it is too large
Load Diff
25
go-zero.dev/cn/dev-flow.md
Normal file
25
go-zero.dev/cn/dev-flow.md
Normal file
@ -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(推荐)
|
25
go-zero.dev/cn/dev-specification.md
Normal file
25
go-zero.dev/cn/dev-specification.md
Normal file
@ -0,0 +1,25 @@
|
||||
# 开发规范
|
||||
在实际业务开发中,除了要提高业务开发效率,缩短业务开发周期,保证线上业务高性能,高可用的指标外,好的编程习惯也是一个开发人员基本素养之一,在本章节,
|
||||
我们将介绍一下go-zero中的编码规范,本章节为可选章节,内容仅供交流与参考,本章节将从以下小节进行说明:
|
||||
* [命名规范](naming-spec.md)
|
||||
* [路由规范](route-naming-spec.md)
|
||||
* [编码规范](coding-spec.md)
|
||||
|
||||
## 开发三原则
|
||||
|
||||
### Clarity(清晰)
|
||||
作者引用了`Hal Abelson and Gerald Sussman`的一句话:
|
||||
> Programs must be written for people to read, and only incidentally for machines to execute
|
||||
|
||||
程序是什么,程序必须是为了开发人员阅读而编写的,只是偶尔给机器去执行,99%的时间程序代码面向的是开发人员,而只有1%的时间可能是机器在执行,这里比例不是重点,从中我们可以看出,清晰的代码是多么的重要,因为所有程序,不仅是Go语言,都是由开发人员编写,供其他人阅读和维护。
|
||||
|
||||
|
||||
### Simplicity(简单)
|
||||
> Simplicity is prerequisite for reliability
|
||||
|
||||
`Edsger W. Dijkstra`认为:可靠的前提条件就是简单,我们在实际开发中都遇到过,这段代码在写什么,想要完成什么事情,开发人员不理解这段代码,因此也不知道如何去维护,这就带来了复杂性,程序越是复杂就越难维护,越难维护就会是程序变得越来越复杂,因此,遇到程序变复杂时首先应该想到的是——重构,重构会重新设计程序,让程序变得简单。
|
||||
|
||||
### Productivity(生产力)
|
||||
在go-zero团队中,一直在强调这个话题,开发人员成产力的多少,并不是你写了多少行代码,完成了多少个模块开发,而是我们需要利用各种有效的途径来利用有限的时间完成开发效率最大化,而Goctl的诞生正是为了提高生产力,
|
||||
因此这个开发原则我是非常认同的。
|
||||
|
55
go-zero.dev/cn/doc-contibute.md
Normal file
55
go-zero.dev/cn/doc-contibute.md
Normal file
@ -0,0 +1,55 @@
|
||||
# 文档贡献
|
||||
|
||||
## 怎么贡献文档?
|
||||
点击顶部"编辑此页"按钮即可进入源码仓库对应的文件,开发人员将修改(添加)的文档通过pr形式提交,
|
||||
我们收到pr后会进行文档审核,一旦审核通过即可更新文档。
|
||||
![doc-edit](./resource/doc-edit.png)
|
||||
|
||||
## 可以贡献哪些文档?
|
||||
* 文档编写错误
|
||||
* 文档不规范、不完整
|
||||
* go-zero应用实践、心得
|
||||
* 组件中心
|
||||
|
||||
## 文档pr通过后文档多久会更新?
|
||||
在pr接受后,github action会自动build gitbook并发布,因此在github action成功后1-2分钟即可查看更新后的文档。
|
||||
|
||||
## 文档贡献注意事项
|
||||
* 纠错、完善源文档可以直接编写原来的md文件
|
||||
* 新增组件文档需要保证文档排版、易读,且组件文档需要放在[组件中心](extended-reading.md)子目录中
|
||||
* go-zero应用实践分享可以直接放在[开发实践](practise.md)子目录下
|
||||
|
||||
## 目录结构规范
|
||||
* 目录结构不宜过深,最好不要超过3层
|
||||
* 组件文档需要在归属到[组件中心](extended-reading.md),如
|
||||
```markdown
|
||||
* [开发实践](practise.md)
|
||||
* [logx](logx.md)
|
||||
* [bloom](bloom.md)
|
||||
* [executors](executors.md)
|
||||
* 你的文档目录名称
|
||||
```
|
||||
* 应用实践需要归属到[开发实践](practise.md),如
|
||||
```markdown
|
||||
* [开发实践](practise.md)
|
||||
* [我是如何用go-zero 实现一个中台系统](datacenter.md)
|
||||
* [流数据处理利器](stream.md)
|
||||
* [10月3日线上交流问题汇总](online-exchange.md
|
||||
* 你的文档目录名称
|
||||
```
|
||||
|
||||
## 开发实践文档模板
|
||||
```markdown
|
||||
# 标题
|
||||
|
||||
> 作者:填入作者名称
|
||||
>
|
||||
> 原文连接: 原文连接
|
||||
|
||||
some markdown content
|
||||
```
|
||||
|
||||
# 猜你想看
|
||||
* [怎么参与贡献](join-us.md)
|
||||
* [Github Pull request](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/proposing-changes-to-your-work-with-pull-requests)
|
||||
|
175
go-zero.dev/cn/error-handle.md
Normal file
175
go-zero.dev/cn/error-handle.md
Normal file
@ -0,0 +1,175 @@
|
||||
# 错误处理
|
||||
错误的处理是一个服务必不可缺的环节。在平时的业务开发中,我们可以认为http状态码不为`2xx`系列的,都可以认为是http请求错误,
|
||||
并伴随响应的错误信息,但这些错误信息都是以plain text形式返回的。除此之外,我在业务中还会定义一些业务性错误,常用做法都是通过
|
||||
`code`、`msg` 两个字段来进行业务处理结果描述,并且希望能够以json响应体来进行响应。
|
||||
|
||||
## 业务错误响应格式
|
||||
* 业务处理正常
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"msg": "successful",
|
||||
"data": {
|
||||
....
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* 业务处理异常
|
||||
```json
|
||||
{
|
||||
"code": 10001,
|
||||
"msg": "参数错误"
|
||||
}
|
||||
```
|
||||
|
||||
## user api之login
|
||||
在之前,我们在登录逻辑中处理用户名不存在时,直接返回来一个error。我们来登录并传递一个不存在的用户名看看效果。
|
||||
```shell
|
||||
curl -X POST \
|
||||
http://127.0.0.1:8888/user/login \
|
||||
-H 'content-type: application/json' \
|
||||
-d '{
|
||||
"username":"1",
|
||||
"password":"123456"
|
||||
}'
|
||||
```
|
||||
```text
|
||||
HTTP/1.1 400 Bad Request
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
X-Content-Type-Options: nosniff
|
||||
Date: Tue, 09 Feb 2021 06:38:42 GMT
|
||||
Content-Length: 19
|
||||
|
||||
用户名不存在
|
||||
```
|
||||
接下来我们将其以json格式进行返回
|
||||
|
||||
## 自定义错误
|
||||
* 首先在common中添加一个`baseerror.go`文件,并填入代码
|
||||
```shell
|
||||
$ cd common
|
||||
$ mkdir errorx&&cd errorx
|
||||
$ vim baseerror.go
|
||||
```
|
||||
```goalng
|
||||
package errorx
|
||||
|
||||
const defaultCode = 1001
|
||||
|
||||
type CodeError struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
type CodeErrorResponse struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
func NewCodeError(code int, msg string) error {
|
||||
return &CodeError{Code: code, Msg: msg}
|
||||
}
|
||||
|
||||
func NewDefaultError(msg string) error {
|
||||
return NewCodeError(defaultCode, msg)
|
||||
}
|
||||
|
||||
func (e *CodeError) Error() string {
|
||||
return e.Msg
|
||||
}
|
||||
|
||||
func (e *CodeError) Data() *CodeErrorResponse {
|
||||
return &CodeErrorResponse{
|
||||
Code: e.Code,
|
||||
Msg: e.Msg,
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
* 将登录逻辑中错误用CodeError自定义错误替换
|
||||
```go
|
||||
if len(strings.TrimSpace(req.Username)) == 0 || len(strings.TrimSpace(req.Password)) == 0 {
|
||||
return nil, errorx.NewDefaultError("参数错误")
|
||||
}
|
||||
|
||||
userInfo, err := l.svcCtx.UserModel.FindOneByNumber(req.Username)
|
||||
switch err {
|
||||
case nil:
|
||||
case model.ErrNotFound:
|
||||
return nil, errorx.NewDefaultError("用户名不存在")
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if userInfo.Password != req.Password {
|
||||
return nil, errorx.NewDefaultError("用户密码不正确")
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
accessExpire := l.svcCtx.Config.Auth.AccessExpire
|
||||
jwtToken, err := l.getJwtToken(l.svcCtx.Config.Auth.AccessSecret, now, l.svcCtx.Config.Auth.AccessExpire, userInfo.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &types.LoginReply{
|
||||
Id: userInfo.Id,
|
||||
Name: userInfo.Name,
|
||||
Gender: userInfo.Gender,
|
||||
AccessToken: jwtToken,
|
||||
AccessExpire: now + accessExpire,
|
||||
RefreshAfter: now + accessExpire/2,
|
||||
}, nil
|
||||
```
|
||||
|
||||
* 开启自定义错误
|
||||
```shell
|
||||
$ vim service/user/cmd/api/user.go
|
||||
```
|
||||
```go
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
var c config.Config
|
||||
conf.MustLoad(*configFile, &c)
|
||||
|
||||
ctx := svc.NewServiceContext(c)
|
||||
server := rest.MustNewServer(c.RestConf)
|
||||
defer server.Stop()
|
||||
|
||||
handler.RegisterHandlers(server, ctx)
|
||||
|
||||
// 自定义错误
|
||||
httpx.SetErrorHandler(func(err error) (int, interface{}) {
|
||||
switch e := err.(type) {
|
||||
case *errorx.CodeError:
|
||||
return http.StatusOK, e.Data()
|
||||
default:
|
||||
return http.StatusInternalServerError, nil
|
||||
}
|
||||
})
|
||||
|
||||
fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port)
|
||||
server.Start()
|
||||
}
|
||||
```
|
||||
* 重启服务验证
|
||||
```shell
|
||||
$ curl -i -X POST \
|
||||
http://127.0.0.1:8888/user/login \
|
||||
-H 'content-type: application/json' \
|
||||
-d '{
|
||||
"username":"1",
|
||||
"password":"123456"
|
||||
}'
|
||||
```
|
||||
```text
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
Date: Tue, 09 Feb 2021 06:47:29 GMT
|
||||
Content-Length: 40
|
||||
|
||||
{"code":1001,"msg":"用户名不存在"}
|
||||
```
|
46
go-zero.dev/cn/error.md
Normal file
46
go-zero.dev/cn/error.md
Normal file
@ -0,0 +1,46 @@
|
||||
# 常见错误处理
|
||||
|
||||
## Windows上报错
|
||||
```text
|
||||
A required privilege is not held by the client.
|
||||
```
|
||||
解决方法:"以管理员身份运行" goctl 即可。
|
||||
|
||||
## grpc引起错误
|
||||
* 错误一
|
||||
```text
|
||||
protoc-gen-go: unable to determine Go import path for "greet.proto"
|
||||
|
||||
Please specify either:
|
||||
• a "go_package" option in the .proto source file, or
|
||||
• a "M" argument on the command line.
|
||||
|
||||
See https://developers.google.com/protocol-buffers/docs/reference/go-generated#package for more information.
|
||||
|
||||
--go_out: protoc-gen-go: Plugin failed with status code 1.
|
||||
|
||||
```
|
||||
解决方法:
|
||||
```text
|
||||
go get -u github.com/golang/protobuf/protoc-gen-go@v1.3.2
|
||||
```
|
||||
|
||||
## protoc-gen-go安装失败
|
||||
```text
|
||||
go get github.com/golang/protobuf/protoc-gen-go: module github.com/golang/protobuf/protoc-gen-go: Get "https://proxy.golang.org/github.com/golang/protobuf/protoc-gen-go/@v/list": dial tcp 216.58.200.49:443: i/o timeout
|
||||
```
|
||||
|
||||
请确认`GOPROXY`已经设置,GOPROXY设置见[go module配置](gomod-config.md)
|
||||
|
||||
## api服务启动失败
|
||||
```text
|
||||
error: config file etc/user-api.yaml, error: type mismatch for field xx
|
||||
```
|
||||
|
||||
请确认`user-api.yaml`配置文件中配置项是否已经配置,如果有值,检查一下yaml配置文件是否符合yaml格式。
|
||||
|
||||
## goctl找不到
|
||||
```
|
||||
command not found: goctl
|
||||
```
|
||||
请确保goctl已经安装或者goctl是否已经添加到环境变量
|
325
go-zero.dev/cn/executors.md
Normal file
325
go-zero.dev/cn/executors.md
Normal file
@ -0,0 +1,325 @@
|
||||
# executors
|
||||
|
||||
在 `go-zero` 中,`executors` 充当任务池,做多任务缓冲,适用于做批量处理的任务。如:`clickhouse` 大批量 `insert`,`sql batch insert`。同时也可以在 `go-queue` 中看到 `executors` 【在 `queue` 里面使用的是 `ChunkExecutor` ,限定任务提交字节大小】。
|
||||
|
||||
所以当你存在以下需求,都可以使用这个组件:
|
||||
|
||||
- 批量提交任务
|
||||
- 缓冲一部分任务,惰性提交
|
||||
- 延迟任务提交
|
||||
|
||||
|
||||
|
||||
具体解释之前,先给一个大致的概览图:
|
||||
![c42c34e8d33d48ec8a63e56feeae882a](./resource/c42c34e8d33d48ec8a63e56feeae882a.png)
|
||||
## 接口设计
|
||||
|
||||
|
||||
在 `executors` 包下,有如下几个 `executor` :
|
||||
|
||||
| Name | Margin value |
|
||||
| --- | --- |
|
||||
| `bulkexecutor` | 达到 `maxTasks` 【最大任务数】 提交 |
|
||||
| `chunkexecutor` | 达到 `maxChunkSize`【最大字节数】提交 |
|
||||
| `periodicalexecutor` | `basic executor` |
|
||||
| `delayexecutor` | 延迟执行传入的 `fn()` |
|
||||
| `lessexecutor` | |
|
||||
|
||||
|
||||
|
||||
你会看到除了有特殊功能的 `delay`,`less` ,其余 3 个都是 `executor` + `container` 的组合设计:
|
||||
|
||||
|
||||
```go
|
||||
func NewBulkExecutor(execute Execute, opts ...BulkOption) *BulkExecutor {
|
||||
// 选项模式:在 go-zero 中多处出现。在多配置下,比较好的设计思路
|
||||
// https://halls-of-valhalla.org/beta/articles/functional-options-pattern-in-go,54/
|
||||
options := newBulkOptions()
|
||||
for _, opt := range opts {
|
||||
opt(&options)
|
||||
}
|
||||
// 1. task container: [execute 真正做执行的函数] [maxTasks 执行临界点]
|
||||
container := &bulkContainer{
|
||||
execute: execute,
|
||||
maxTasks: options.cachedTasks,
|
||||
}
|
||||
// 2. 可以看出 bulkexecutor 底层依赖 periodicalexecutor
|
||||
executor := &BulkExecutor{
|
||||
executor: NewPeriodicalExecutor(options.flushInterval, container),
|
||||
container: container,
|
||||
}
|
||||
|
||||
return executor
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
而这个 `container`是个 `interface`:
|
||||
|
||||
|
||||
```go
|
||||
TaskContainer interface {
|
||||
// 把 task 加入 container
|
||||
AddTask(task interface{}) bool
|
||||
// 实际上是去执行传入的 execute func()
|
||||
Execute(tasks interface{})
|
||||
// 达到临界值,移除 container 中全部的 task,通过 channel 传递到 execute func() 执行
|
||||
RemoveAll() interface{}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
由此可见之间的依赖关系:
|
||||
|
||||
|
||||
- `bulkexecutor`:`periodicalexecutor` + `bulkContainer`
|
||||
- `chunkexecutor`:`periodicalexecutor` + `chunkContainer`
|
||||
|
||||
|
||||
> [!TIP]
|
||||
> 所以你想完成自己的 `executor`,可以实现 `container` 的这 3 个接口,再结合 `periodicalexecutor` 就行
|
||||
|
||||
所以回到👆那张图,我们的重点就放在 `periodicalexecutor`,看看它是怎么设计的?
|
||||
|
||||
|
||||
## 如何使用
|
||||
|
||||
|
||||
首先看看如何在业务中使用这个组件:
|
||||
|
||||
现有一个定时服务,每天固定时间去执行从 `mysql` 到 `clickhouse` 的数据同步:
|
||||
|
||||
|
||||
```go
|
||||
type DailyTask struct {
|
||||
ckGroup *clickhousex.Cluster
|
||||
insertExecutor *executors.BulkExecutor
|
||||
mysqlConn sqlx.SqlConn
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
初始化 `bulkExecutor`:
|
||||
|
||||
```go
|
||||
func (dts *DailyTask) Init() {
|
||||
// insertIntoCk() 是真正insert执行函数【需要开发者自己编写具体业务逻辑】
|
||||
dts.insertExecutor = executors.NewBulkExecutor(
|
||||
dts.insertIntoCk,
|
||||
executors.WithBulkInterval(time.Second*3), // 3s会自动刷一次container中task去执行
|
||||
executors.WithBulkTasks(10240), // container最大task数。一般设为2的幂次
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> 额外介绍一下:`clickhouse` 适合大批量的插入,因为 insert 速度很快,大批量 insert 更能充分利用 clickhouse
|
||||
|
||||
|
||||
主体业务逻辑编写:
|
||||
|
||||
|
||||
```go
|
||||
func (dts *DailyTask) insertNewData(ch chan interface{}, sqlFromDb *model.Task) error {
|
||||
for item := range ch {
|
||||
if r, vok := item.(*model.Task); !vok {
|
||||
continue
|
||||
}
|
||||
err := dts.insertExecutor.Add(r)
|
||||
if err != nil {
|
||||
r.Tag = sqlFromDb.Tag
|
||||
r.TagId = sqlFromDb.Id
|
||||
r.InsertId = genInsertId()
|
||||
r.ToRedis = toRedis == constant.INCACHED
|
||||
r.UpdateWay = sqlFromDb.UpdateWay
|
||||
// 1. Add Task
|
||||
err := dts.insertExecutor.Add(r)
|
||||
if err != nil {
|
||||
logx.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
// 2. Flush Task container
|
||||
dts.insertExecutor.Flush()
|
||||
// 3. Wait All Task Finish
|
||||
dts.insertExecutor.Wait()
|
||||
}
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> 可能会疑惑为什么要 `Flush(), Wait()` ,后面会通过源码解析一下
|
||||
|
||||
使用上总体分为 3 步:
|
||||
|
||||
|
||||
- `Add()`:加入 task
|
||||
- `Flush()`:刷新 `container` 中的 task
|
||||
- `Wait()`:等待全部 task 执行完成
|
||||
|
||||
|
||||
|
||||
## 源码分析
|
||||
|
||||
> [!TIP]
|
||||
> 此处主要分析 `periodicalexecutor`,因为其他两个常用的 `executor` 都依赖它
|
||||
|
||||
|
||||
|
||||
### 初始化
|
||||
|
||||
```go
|
||||
func New...(interval time.Duration, container TaskContainer) *PeriodicalExecutor {
|
||||
executor := &PeriodicalExecutor{
|
||||
commander: make(chan interface{}, 1),
|
||||
interval: interval,
|
||||
container: container,
|
||||
confirmChan: make(chan lang.PlaceholderType),
|
||||
newTicker: func(d time.Duration) timex.Ticker {
|
||||
return timex.NewTicker(interval)
|
||||
},
|
||||
}
|
||||
...
|
||||
return executor
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
- `commander`:传递 `tasks` 的 channel
|
||||
- `container`:暂存 `Add()` 的 task
|
||||
- `confirmChan`:阻塞 `Add()` ,在开始本次的 `executeTasks()` 会放开阻塞
|
||||
- `ticker`:定时器,防止 `Add()` 阻塞时,会有一个定时执行的机会,及时释放暂存的 task
|
||||
|
||||
|
||||
|
||||
### Add()
|
||||
初始化完,在业务逻辑的第一步就是把 task 加入 `executor`:
|
||||
|
||||
```go
|
||||
func (pe *PeriodicalExecutor) Add(task interface{}) {
|
||||
if vals, ok := pe.addAndCheck(task); ok {
|
||||
pe.commander <- vals
|
||||
<-pe.confirmChan
|
||||
}
|
||||
}
|
||||
|
||||
func (pe *PeriodicalExecutor) addAndCheck(task interface{}) (interface{}, bool) {
|
||||
pe.lock.Lock()
|
||||
defer func() {
|
||||
// 一开始为 false
|
||||
var start bool
|
||||
if !pe.guarded {
|
||||
// backgroundFlush() 会将 guarded 重新置反
|
||||
pe.guarded = true
|
||||
start = true
|
||||
}
|
||||
pe.lock.Unlock()
|
||||
// 在第一条 task 加入的时候就会执行 if 中的 backgroundFlush()。后台协程刷task
|
||||
if start {
|
||||
pe.backgroundFlush()
|
||||
}
|
||||
}()
|
||||
// 控制maxTask,>=maxTask 将container中tasks pop, return
|
||||
if pe.container.AddTask(task) {
|
||||
return pe.container.RemoveAll(), true
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
```
|
||||
|
||||
`addAndCheck()` 中 `AddTask()` 就是在控制最大 tasks 数,如果超过就执行 `RemoveAll()` ,将暂存 `container` 的 tasks pop,传递给 `commander` ,后面有 goroutine 循环读取,然后去执行 tasks。
|
||||
|
||||
### backgroundFlush()
|
||||
开启一个后台协程,对 `container` 中的 task,不断刷新:
|
||||
|
||||
```go
|
||||
func (pe *PeriodicalExecutor) backgroundFlush() {
|
||||
// 封装 go func(){}
|
||||
threading.GoSafe(func() {
|
||||
ticker := pe.newTicker(pe.interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
var commanded bool
|
||||
last := timex.Now()
|
||||
for {
|
||||
select {
|
||||
// 从channel拿到 []tasks
|
||||
case vals := <-pe.commander:
|
||||
commanded = true
|
||||
// 实质:wg.Add(1)
|
||||
pe.enterExecution()
|
||||
// 放开 Add() 的阻塞,而且此时暂存区也为空。才开始新的 task 加入
|
||||
pe.confirmChan <- lang.Placeholder
|
||||
// 真正的执行 task 逻辑
|
||||
pe.executeTasks(vals)
|
||||
last = timex.Now()
|
||||
case <-ticker.Chan():
|
||||
if commanded {
|
||||
// 由于select选择的随机性,如果同时满足两个条件同时执行完上面的,此处置反,并跳过本段执行
|
||||
// https://draveness.me/golang/docs/part2-foundation/ch05-keyword/golang-select/
|
||||
commanded = false
|
||||
} else if pe.Flush() {
|
||||
// 刷新完成,定时器清零。暂存区空了,开始下一次定时刷新
|
||||
last = timex.Now()
|
||||
} else if timex.Since(last) > pe.interval*idleRound {
|
||||
// 既没到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/tal-tech/go-zero/commit/9d9399ad1014c171cc9bd9c87f78b5d2ac238ce4)才出现,为什么会这么设计?
|
||||
|
||||
> 之前是:`wg.Add(1)` 是写在 `executeTasks()` ;现在是:先`wg.Add(1)`,再放开 `confirmChan` 阻塞
|
||||
> 如果 `executor func` 执行阻塞,`Add task` 还在进行,因为没有阻塞,可能很快执行到 `Executor.Wait()`,这时就会出现 `wg.Wait()` 在 `wg.Add()` 前执行,这会 `panic`
|
||||
|
||||
具体可以看最新版本的`TestPeriodicalExecutor_WaitFast()` ,不妨跑在此版本上,就可以重现
|
||||
|
||||
## 总结
|
||||
剩余还有几个 `executors` 的分析,就留给大家去看看源码。
|
||||
|
||||
总之,整体设计上:
|
||||
|
||||
- 遵循面向接口设计
|
||||
- 灵活使用 `channel` ,`waitgroup` 等并发工具
|
||||
- 执行单元+存储单元的搭配使用
|
||||
|
||||
在 `go-zero` 中还有很多实用的组件工具,用好工具对于提升服务性能和开发效率都有很大的帮助,希望本篇文章能给大家带来一些收获。
|
30
go-zero.dev/cn/extended-reading.md
Normal file
30
go-zero.dev/cn/extended-reading.md
Normal file
@ -0,0 +1,30 @@
|
||||
# 扩展阅读
|
||||
扩展阅读是对[go-zero](https://github.com/zeromicro/go-zero) 中的最佳实现和组件的介绍,
|
||||
因此会比较庞大,而此资源将会持续更新,也欢迎大家来进行文档贡献,本节将包含以下目录(按照文档更新时间排序):
|
||||
|
||||
* [快速构建高并发微服务](shorturl.md)
|
||||
* [日志组件介绍](logx.md)
|
||||
* [布隆过滤器](bloom.md)
|
||||
* [executors](executors.md)
|
||||
* [流处理组件 fx](fx.md)
|
||||
* [go-zero mysql使用介绍](mysql.md)
|
||||
* [redis锁](redis-lock.md)
|
||||
* [periodlimit限流](periodlimit.md)
|
||||
* [令牌桶限流](tokenlimit.md)
|
||||
* [时间轮介绍](timing-wheel.md)
|
||||
* [熔断原理与实现](breaker-algorithms.md)
|
||||
* [进程内缓存组件 collection.Cache](collection.md)
|
||||
* [高效的关键词替换和敏感词过滤工具](keywords.md)
|
||||
* [服务自适应降载保护设计](loadshedding.md)
|
||||
* [文本序列化和反序列化](mapping.md)
|
||||
* [并发处理工具 MapReduce](mapreduce.md)
|
||||
* [基于prometheus的微服务指标监控](metric.md)
|
||||
* [防止缓存击穿之进程内共享调用](sharedcalls.md)
|
||||
* [DB缓存机制](sql-cache.md)
|
||||
* [zrpc 使用介绍](zrpc.md)
|
||||
* [go-zero缓存设计之持久层缓存](redis-cache.md)
|
||||
* [go-zero缓存设计之业务层缓存](buiness-cache.md)
|
||||
* [go-zero分布式定时任务](go-queue.md)
|
||||
* [我是如何用go-zero 实现一个中台系统](datacenter.md)
|
||||
* [流数据处理利器](stream.md)
|
||||
* [10月3日线上交流问题汇总](online-exchange.md)
|
48
go-zero.dev/cn/faq.md
Normal file
48
go-zero.dev/cn/faq.md
Normal file
@ -0,0 +1,48 @@
|
||||
# 常见问题集合
|
||||
|
||||
1. goctl安装了执行命令却提示 `command not found: goctl` 字样。
|
||||
> 如果你通过 `go get` 方式安装,那么 `goctl` 应该位于 `$GOPATH` 中,
|
||||
> 你可以通过 `go env GOPATH` 查看完整路径,不管你的 `goctl` 是在 `$GOPATH`中,
|
||||
> 还是在其他目录,出现上述问题的原因就是 `goctl` 所在目录不在 `PATH` (环境变量)中所致。
|
||||
|
||||
2. rpc怎么调用
|
||||
> 该问题可以参考快速开始中的[rpc编写与调用](rpc-call.md)介绍,其中有rpc调用的使用逻辑。
|
||||
|
||||
3. proto使用了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写在这里。
|
11
go-zero.dev/cn/framework-design.md
Normal file
11
go-zero.dev/cn/framework-design.md
Normal file
@ -0,0 +1,11 @@
|
||||
# 框架设计
|
||||
|
||||
![整体框架](./resource/architechture.svg)
|
||||
|
||||
本节将从 go-zero 的设计理念,go-zero 服务的最佳实践目录来说明 go-zero 框架的设计,本节将包含以下小节:
|
||||
|
||||
* [go-zero设计理念](go-zero-design.md)
|
||||
* [go-zero特点](go-zero-features.md)
|
||||
* [api语法介绍](api-grammar.md)
|
||||
* [api目录结构](api-dir.md)
|
||||
* [rpc目录结构](rpc-dir.md)
|
314
go-zero.dev/cn/go-queue.md
Normal file
314
go-zero.dev/cn/go-queue.md
Normal file
@ -0,0 +1,314 @@
|
||||
* ### go-zero 分布式定时任务
|
||||
|
||||
|
||||
|
||||
日常任务开发中,我们会有很多异步、批量、定时、延迟任务要处理,go-zero中有go-queue,推荐使用go-queue去处理,go-queue本身也是基于go-zero开发的,其本身是有两种模式
|
||||
|
||||
- dq : 依赖于beanstalkd,分布式,可存储,延迟、定时设置,关机重启可以重新执行,消息不会丢失,使用非常简单,go-queue中使用了redis setnx保证了每条消息只被消费一次,使用场景主要是用来做日常任务使用
|
||||
- kq:依赖于kafka,这个就不多介绍啦,大名鼎鼎的kafka,使用场景主要是做消息队列
|
||||
|
||||
我们主要说一下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`解决重复消费问题。
|
12
go-zero.dev/cn/go-zero-design.md
Normal file
12
go-zero.dev/cn/go-zero-design.md
Normal file
@ -0,0 +1,12 @@
|
||||
# go-zero设计理念
|
||||
|
||||
对于微服务框架的设计,我们期望保障微服务稳定性的同时,也要特别注重研发效率。所以设计之初,我们就有如下一些准则:
|
||||
|
||||
* 保持简单,第一原则
|
||||
* 弹性设计,面向故障编程
|
||||
* 工具大于约定和文档
|
||||
* 高可用
|
||||
* 高并发
|
||||
* 易扩展
|
||||
* 对业务开发友好,封装复杂度
|
||||
* 约束做一件事只有一种方式
|
21
go-zero.dev/cn/go-zero-features.md
Normal file
21
go-zero.dev/cn/go-zero-features.md
Normal file
@ -0,0 +1,21 @@
|
||||
# go-zero特性
|
||||
|
||||
go-zero 是一个集成了各种工程实践的包含 web 和 rpc 框架,有如下主要特点:
|
||||
|
||||
* 强大的工具支持,尽可能少的代码编写
|
||||
* 极简的接口
|
||||
* 完全兼容 net/http
|
||||
* 支持中间件,方便扩展
|
||||
* 高性能
|
||||
* 面向故障编程,弹性设计
|
||||
* 内建服务发现、负载均衡
|
||||
* 内建限流、熔断、降载,且自动触发,自动恢复
|
||||
* API 参数自动校验
|
||||
* 超时级联控制
|
||||
* 自动缓存控制
|
||||
* 链路跟踪、统计报警等
|
||||
* 高并发支撑,稳定保障了疫情期间每天的流量洪峰
|
||||
|
||||
如下图,我们从多个层面保障了整体服务的高可用:
|
||||
|
||||
![弹性设计](https://gitee.com/kevwan/static/raw/master/doc/images/resilience.jpg)
|
69
go-zero.dev/cn/goctl-api.md
Normal file
69
go-zero.dev/cn/goctl-api.md
Normal file
@ -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/tal-tech/go-zero/tree/master/tools/goctl/config/readme.md]
|
||||
```
|
||||
|
||||
* --dir 代码输出目录
|
||||
* --api 指定api源文件
|
||||
* --style 指定生成代码文件的文件名称风格,详情见[文件名称命名style说明](https://github.com/tal-tech/go-zero/tree/master/tools/goctl/config/readme.md)
|
||||
|
||||
## 使用示例
|
||||
```shell
|
||||
$ goctl api go -api user.api -dir . -style gozero
|
||||
```
|
||||
|
||||
|
||||
# 猜你想看
|
||||
* [api语法](api-grammar.md)
|
||||
* [api目录](api-dir.md)
|
271
go-zero.dev/cn/goctl-commands.md
Normal file
271
go-zero.dev/cn/goctl-commands.md
Normal file
@ -0,0 +1,271 @@
|
||||
# goctl命令大全
|
||||
![goctl](https://zeromicro.github.io/go-zero/cn/resource/goctl-command.png)
|
||||
|
||||
# goctl
|
||||
|
||||
## api
|
||||
(api服务相关操作)
|
||||
|
||||
### -o
|
||||
(生成api文件)
|
||||
|
||||
- 示例:goctl api -o user.api
|
||||
|
||||
### new
|
||||
(快速创建一个api服务)
|
||||
|
||||
- 示例:goctl api new user
|
||||
|
||||
### format
|
||||
(api格式化,vscode使用)
|
||||
|
||||
- -dir
|
||||
(目标目录)
|
||||
- -iu
|
||||
(是否自动更新goctl)
|
||||
- -stdin
|
||||
(是否从标准输入读取数据)
|
||||
|
||||
### validate
|
||||
(验证api文件是否有效)
|
||||
|
||||
- -api
|
||||
(指定api文件源)
|
||||
|
||||
- 示例:goctl api validate -api user.api
|
||||
|
||||
### doc
|
||||
(生成doc markdown)
|
||||
|
||||
- -dir
|
||||
(指定目录)
|
||||
|
||||
- 示例:goctl api doc -dir user
|
||||
|
||||
### go
|
||||
(生成golang api服务)
|
||||
|
||||
- -dir
|
||||
(指定代码存放目录)
|
||||
- -api
|
||||
(指定api文件源)
|
||||
- -force
|
||||
(是否强制覆盖已经存在的文件)
|
||||
- -style
|
||||
(指定文件名命名风格,gozero:小写,go_zero:下划线,GoZero:驼峰)
|
||||
|
||||
### java
|
||||
(生成访问api服务代码-java语言)
|
||||
|
||||
- -dir
|
||||
(指定代码存放目录)
|
||||
- -api
|
||||
(指定api文件源)
|
||||
|
||||
### ts
|
||||
(生成访问api服务代码-ts语言)
|
||||
|
||||
- -dir
|
||||
(指定代码存放目录)
|
||||
- -api
|
||||
(指定api文件源)
|
||||
- webapi
|
||||
- caller
|
||||
- unwrap
|
||||
|
||||
### dart
|
||||
(生成访问api服务代码-dart语言)
|
||||
|
||||
- -dir
|
||||
(指定代码存放目标)
|
||||
- -api
|
||||
(指定api文件源)
|
||||
|
||||
### kt
|
||||
(生成访问api服务代码-kotlin语言)
|
||||
|
||||
- -dir
|
||||
(指定代码存放目标)
|
||||
- -api
|
||||
(指定api文件源)
|
||||
- -pkg
|
||||
(指定包名)
|
||||
|
||||
### plugin
|
||||
|
||||
- -plugin
|
||||
可执行文件
|
||||
- -dir
|
||||
代码存放目标文件夹
|
||||
- -api
|
||||
api源码文件
|
||||
- -style
|
||||
文件名命名格式化
|
||||
|
||||
## template
|
||||
(模板操作)
|
||||
|
||||
### init
|
||||
(缓存api/rpc/model模板)
|
||||
|
||||
- 示例:goctl template init
|
||||
|
||||
### clean
|
||||
(清空缓存模板)
|
||||
|
||||
- 示例:goctl template clean
|
||||
|
||||
### update
|
||||
(更新模板)
|
||||
|
||||
- -category,c
|
||||
(指定需要更新的分组名 api|rpc|model)
|
||||
|
||||
- 示例:goctl template update -c api
|
||||
|
||||
### revert
|
||||
(还原指定模板文件)
|
||||
|
||||
- -category,c
|
||||
(指定需要更新的分组名 api|rpc|model)
|
||||
- -name,n
|
||||
(指定模板文件名)
|
||||
|
||||
## config
|
||||
(配置文件生成)
|
||||
|
||||
### -path,p
|
||||
(指定配置文件存放目录)
|
||||
|
||||
- 示例:goctl config -p user
|
||||
|
||||
## docker
|
||||
(生成Dockerfile)
|
||||
|
||||
### -go
|
||||
(指定main函数文件)
|
||||
|
||||
### -port
|
||||
(指定暴露端口)
|
||||
|
||||
## rpc (rpc服务相关操作)
|
||||
|
||||
### new
|
||||
(快速生成一个rpc服务)
|
||||
|
||||
- -idea
|
||||
(标识命令是否来源于idea插件,用于idea插件开发使用,终端执行请忽略[可选参数])
|
||||
- -style
|
||||
(指定文件名命名风格,gozero:小写,go_zero:下划线,GoZero:驼峰)
|
||||
|
||||
### template
|
||||
(创建一个proto模板文件)
|
||||
|
||||
- -idea
|
||||
(标识命令是否来源于idea插件,用于idea插件开发使用,终端执行请忽略[可选参数])
|
||||
- -out,o
|
||||
(指定代码存放目录)
|
||||
|
||||
### proto
|
||||
(根据proto生成rpc服务)
|
||||
|
||||
- -src,s
|
||||
(指定proto文件源)
|
||||
- -proto_path,I
|
||||
(指定proto import查找目录,protoc原生命令,具体用法可参考protoc -h查看)
|
||||
- -dir,d
|
||||
(指定代码存放目录)
|
||||
- -idea
|
||||
(标识命令是否来源于idea插件,用于idea插件开发使用,终端执行请忽略[可选参数])
|
||||
- -style
|
||||
(指定文件名命名风格,gozero:小写,go_zero:下划线,GoZero:驼峰)
|
||||
|
||||
### model
|
||||
(model层代码操作)
|
||||
|
||||
- mysql
|
||||
(从mysql生成model代码)
|
||||
|
||||
- ddl
|
||||
(指定数据源为
|
||||
ddl文件生成model代码)
|
||||
|
||||
- -src,s
|
||||
(指定包含ddl的sql文件源,支持通配符匹配)
|
||||
- -dir,d
|
||||
(指定代码存放目录)
|
||||
- -style
|
||||
(指定文件名命名风格,gozero:小写,go_zero:下划线,GoZero:驼峰)
|
||||
- -cache,c
|
||||
(生成代码是否带redis缓存逻辑,bool值)
|
||||
- -idea
|
||||
(标识命令是否来源于idea插件,用于idea插件开发使用,终端执行请忽略[可选参数])
|
||||
|
||||
- datasource
|
||||
(指定数据源从
|
||||
数据库链接生成model代码)
|
||||
|
||||
- -url
|
||||
(指定数据库链接)
|
||||
- -table,t
|
||||
(指定表名,支持通配符)
|
||||
- -dir,d
|
||||
(指定代码存放目录)
|
||||
- -style
|
||||
(指定文件名命名风格,gozero:小写,go_zero:下划线,GoZero:驼峰)
|
||||
- -cache,c
|
||||
(生成代码是否带redis缓存逻辑,bool值)
|
||||
- -idea
|
||||
(标识命令是否来源于idea插件,用于idea插件开发使用,终端执行请忽略[可选参数])
|
||||
- mongo
|
||||
(从mongo生成model代码)
|
||||
|
||||
- -type,t
|
||||
(指定Go Type名称)
|
||||
- -cache,c
|
||||
(生成代码是否带redis缓存逻辑,bool值,默认否)
|
||||
- -dir,d
|
||||
(指定代码生成目录)
|
||||
- -style
|
||||
(指定文件名命名风格,gozero:小写,go_zero:下划线,GoZero:驼峰)
|
||||
|
||||
## upgrade
|
||||
goctl更新到最新版本
|
||||
|
||||
## kube
|
||||
生成k8s部署文件
|
||||
|
||||
### deploy
|
||||
|
||||
|
||||
- -name
|
||||
服务名称
|
||||
- -namespace
|
||||
指定k8s namespace
|
||||
- -image
|
||||
指定镜像名称
|
||||
- -secret
|
||||
指定获取镜像的k8s secret
|
||||
- -requestCpu
|
||||
指定cpu默认分配额
|
||||
- -requestMem
|
||||
指定内存默认分配额
|
||||
- -limitCpu
|
||||
指定cpu最大分配额
|
||||
- -limitMem
|
||||
指定内存最大分配额
|
||||
- -o
|
||||
deployment.yaml输出目录
|
||||
- -replicas
|
||||
指定副本数
|
||||
- -revisions
|
||||
指定保留发布记录数
|
||||
- -port
|
||||
指定服务端口
|
||||
- -nodePort
|
||||
指定服务对外暴露端口
|
||||
- -minReplicas
|
||||
指定最小副本数
|
||||
- -maxReplicas
|
||||
指定最大副本数
|
||||
|
34
go-zero.dev/cn/goctl-install.md
Normal file
34
go-zero.dev/cn/goctl-install.md
Normal file
@ -0,0 +1,34 @@
|
||||
# Goctl安装
|
||||
|
||||
## 前言
|
||||
Goctl在go-zero项目开发着有着很大的作用,其可以有效的帮助开发者大大提高开发效率,减少代码的出错率,缩短业务开发的工作量,更多的Goctl的介绍请阅读[Goctl介绍](goctl.md),
|
||||
在这里我们强烈推荐大家安装,因为后续演示例子中我们大部分都会以goctl进行演示。
|
||||
|
||||
## 安装(mac&linux)
|
||||
* download&install
|
||||
```shell
|
||||
GO111MODULE=on GOPROXY=https://goproxy.cn/,direct go get -u github.com/tal-tech/go-zero/tools/goctl
|
||||
```
|
||||
* 环境变量检测
|
||||
|
||||
`go get`下载编译后的二进制文件位于`$GOPATH/bin`目录下,要确保`$GOPATH/bin`已经添加到环境变量。
|
||||
```shell
|
||||
$ sudo vim /etc/paths
|
||||
```
|
||||
在最后一行添加如下内容
|
||||
```text
|
||||
$GOPATH/bin
|
||||
```
|
||||
> [!TIP]
|
||||
> `$GOPATH`为你本机上的文件地址
|
||||
|
||||
* 安装结果验证
|
||||
```shell
|
||||
$ goctl -v
|
||||
```
|
||||
```text
|
||||
goctl version 1.1.4 darwin/amd64
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> windows用户添加环境变量请自行google
|
374
go-zero.dev/cn/goctl-model.md
Normal file
374
go-zero.dev/cn/goctl-model.md
Normal file
@ -0,0 +1,374 @@
|
||||
# model命令
|
||||
|
||||
goctl model 为go-zero下的工具模块中的组件之一,目前支持识别mysql ddl进行model层代码生成,通过命令行或者idea插件(即将支持)可以有选择地生成带redis cache或者不带redis cache的代码逻辑。
|
||||
|
||||
## 快速开始
|
||||
|
||||
* 通过ddl生成
|
||||
|
||||
```shell
|
||||
$ goctl model mysql ddl -src="./*.sql" -dir="./sql/model" -c
|
||||
```
|
||||
|
||||
执行上述命令后即可快速生成CURD代码。
|
||||
|
||||
```text
|
||||
model
|
||||
│ ├── error.go
|
||||
│ └── usermodel.go
|
||||
```
|
||||
|
||||
* 通过datasource生成
|
||||
|
||||
```shell
|
||||
$ goctl model mysql datasource -url="user:password@tcp(127.0.0.1:3306)/database" -table="*" -dir="./model"
|
||||
```
|
||||
|
||||
* 生成代码示例
|
||||
```go
|
||||
package model
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tal-tech/go-zero/core/stores/cache"
|
||||
"github.com/tal-tech/go-zero/core/stores/sqlc"
|
||||
"github.com/tal-tech/go-zero/core/stores/sqlx"
|
||||
"github.com/tal-tech/go-zero/core/stringx"
|
||||
"github.com/tal-tech/go-zero/tools/goctl/model/sql/builderx"
|
||||
)
|
||||
|
||||
var (
|
||||
userFieldNames = builderx.RawFieldNames(&User{})
|
||||
userRows = strings.Join(userFieldNames, ",")
|
||||
userRowsExpectAutoSet = strings.Join(stringx.Remove(userFieldNames, "`id`", "`create_time`", "`update_time`"), ",")
|
||||
userRowsWithPlaceHolder = strings.Join(stringx.Remove(userFieldNames, "`id`", "`create_time`", "`update_time`"), "=?,") + "=?"
|
||||
|
||||
cacheUserNamePrefix = "cache#User#name#"
|
||||
cacheUserMobilePrefix = "cache#User#mobile#"
|
||||
cacheUserIdPrefix = "cache#User#id#"
|
||||
cacheUserPrefix = "cache#User#user#"
|
||||
)
|
||||
|
||||
type (
|
||||
UserModel interface {
|
||||
Insert(data User) (sql.Result, error)
|
||||
FindOne(id int64) (*User, error)
|
||||
FindOneByUser(user string) (*User, error)
|
||||
FindOneByName(name string) (*User, error)
|
||||
FindOneByMobile(mobile string) (*User, error)
|
||||
Update(data User) error
|
||||
Delete(id int64) error
|
||||
}
|
||||
|
||||
defaultUserModel struct {
|
||||
sqlc.CachedConn
|
||||
table string
|
||||
}
|
||||
|
||||
User struct {
|
||||
Id int64 `db:"id"`
|
||||
User string `db:"user"` // 用户
|
||||
Name string `db:"name"` // 用户名称
|
||||
Password string `db:"password"` // 用户密码
|
||||
Mobile string `db:"mobile"` // 手机号
|
||||
Gender string `db:"gender"` // 男|女|未公开
|
||||
Nickname string `db:"nickname"` // 用户昵称
|
||||
CreateTime time.Time `db:"create_time"`
|
||||
UpdateTime time.Time `db:"update_time"`
|
||||
}
|
||||
)
|
||||
|
||||
func NewUserModel(conn sqlx.SqlConn, c cache.CacheConf) UserModel {
|
||||
return &defaultUserModel{
|
||||
CachedConn: sqlc.NewConn(conn, c),
|
||||
table: "`user`",
|
||||
}
|
||||
}
|
||||
|
||||
func (m *defaultUserModel) Insert(data User) (sql.Result, error) {
|
||||
userNameKey := fmt.Sprintf("%s%v", cacheUserNamePrefix, data.Name)
|
||||
userMobileKey := fmt.Sprintf("%s%v", cacheUserMobilePrefix, data.Mobile)
|
||||
userKey := fmt.Sprintf("%s%v", cacheUserPrefix, data.User)
|
||||
ret, err := m.Exec(func(conn sqlx.SqlConn) (result sql.Result, err error) {
|
||||
query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?)", m.table, userRowsExpectAutoSet)
|
||||
return conn.Exec(query, data.User, data.Name, data.Password, data.Mobile, data.Gender, data.Nickname)
|
||||
}, userNameKey, userMobileKey, userKey)
|
||||
return ret, err
|
||||
}
|
||||
|
||||
func (m *defaultUserModel) FindOne(id int64) (*User, error) {
|
||||
userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, id)
|
||||
var resp User
|
||||
err := m.QueryRow(&resp, userIdKey, func(conn sqlx.SqlConn, v interface{}) error {
|
||||
query := fmt.Sprintf("select %s from %s where `id` = ? limit 1", userRows, m.table)
|
||||
return conn.QueryRow(v, query, id)
|
||||
})
|
||||
switch err {
|
||||
case nil:
|
||||
return &resp, nil
|
||||
case sqlc.ErrNotFound:
|
||||
return nil, ErrNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (m *defaultUserModel) FindOneByUser(user string) (*User, error) {
|
||||
userKey := fmt.Sprintf("%s%v", cacheUserPrefix, user)
|
||||
var resp User
|
||||
err := m.QueryRowIndex(&resp, userKey, m.formatPrimary, func(conn sqlx.SqlConn, v interface{}) (i interface{}, e error) {
|
||||
query := fmt.Sprintf("select %s from %s where `user` = ? limit 1", userRows, m.table)
|
||||
if err := conn.QueryRow(&resp, query, user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Id, nil
|
||||
}, m.queryPrimary)
|
||||
switch err {
|
||||
case nil:
|
||||
return &resp, nil
|
||||
case sqlc.ErrNotFound:
|
||||
return nil, ErrNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (m *defaultUserModel) FindOneByName(name string) (*User, error) {
|
||||
userNameKey := fmt.Sprintf("%s%v", cacheUserNamePrefix, name)
|
||||
var resp User
|
||||
err := m.QueryRowIndex(&resp, userNameKey, m.formatPrimary, func(conn sqlx.SqlConn, v interface{}) (i interface{}, e error) {
|
||||
query := fmt.Sprintf("select %s from %s where `name` = ? limit 1", userRows, m.table)
|
||||
if err := conn.QueryRow(&resp, query, name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Id, nil
|
||||
}, m.queryPrimary)
|
||||
switch err {
|
||||
case nil:
|
||||
return &resp, nil
|
||||
case sqlc.ErrNotFound:
|
||||
return nil, ErrNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (m *defaultUserModel) FindOneByMobile(mobile string) (*User, error) {
|
||||
userMobileKey := fmt.Sprintf("%s%v", cacheUserMobilePrefix, mobile)
|
||||
var resp User
|
||||
err := m.QueryRowIndex(&resp, userMobileKey, m.formatPrimary, func(conn sqlx.SqlConn, v interface{}) (i interface{}, e error) {
|
||||
query := fmt.Sprintf("select %s from %s where `mobile` = ? limit 1", userRows, m.table)
|
||||
if err := conn.QueryRow(&resp, query, mobile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Id, nil
|
||||
}, m.queryPrimary)
|
||||
switch err {
|
||||
case nil:
|
||||
return &resp, nil
|
||||
case sqlc.ErrNotFound:
|
||||
return nil, ErrNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (m *defaultUserModel) Update(data User) error {
|
||||
userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, data.Id)
|
||||
_, err := m.Exec(func(conn sqlx.SqlConn) (result sql.Result, err error) {
|
||||
query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, userRowsWithPlaceHolder)
|
||||
return conn.Exec(query, data.User, data.Name, data.Password, data.Mobile, data.Gender, data.Nickname, data.Id)
|
||||
}, userIdKey)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *defaultUserModel) Delete(id int64) error {
|
||||
data, err := m.FindOne(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userNameKey := fmt.Sprintf("%s%v", cacheUserNamePrefix, data.Name)
|
||||
userMobileKey := fmt.Sprintf("%s%v", cacheUserMobilePrefix, data.Mobile)
|
||||
userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, id)
|
||||
userKey := fmt.Sprintf("%s%v", cacheUserPrefix, data.User)
|
||||
_, err = m.Exec(func(conn sqlx.SqlConn) (result sql.Result, err error) {
|
||||
query := fmt.Sprintf("delete from %s where `id` = ?", m.table)
|
||||
return conn.Exec(query, id)
|
||||
}, userNameKey, userMobileKey, userIdKey, userKey)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *defaultUserModel) formatPrimary(primary interface{}) string {
|
||||
return fmt.Sprintf("%s%v", cacheUserIdPrefix, primary)
|
||||
}
|
||||
|
||||
func (m *defaultUserModel) queryPrimary(conn sqlx.SqlConn, v, primary interface{}) error {
|
||||
query := fmt.Sprintf("select %s from %s where `id` = ? limit 1", userRows, m.table)
|
||||
return conn.QueryRow(v, query, primary)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 用法
|
||||
|
||||
```text
|
||||
$ goctl model mysql -h
|
||||
```
|
||||
|
||||
```text
|
||||
NAME:
|
||||
goctl model mysql - generate mysql model"
|
||||
|
||||
USAGE:
|
||||
goctl model mysql command [command options] [arguments...]
|
||||
|
||||
COMMANDS:
|
||||
ddl generate mysql model from ddl"
|
||||
datasource generate model from datasource"
|
||||
|
||||
OPTIONS:
|
||||
--help, -h show help
|
||||
```
|
||||
|
||||
## 生成规则
|
||||
|
||||
* 默认规则
|
||||
|
||||
我们默认用户在建表时会创建createTime、updateTime字段(忽略大小写、下划线命名风格)且默认值均为`CURRENT_TIMESTAMP`,而updateTime支持`ON UPDATE CURRENT_TIMESTAMP`,对于这两个字段生成`insert`、`update`时会被移除,不在赋值范畴内,当然,如果你不需要这两个字段那也无大碍。
|
||||
* 带缓存模式
|
||||
* ddl
|
||||
|
||||
```shell
|
||||
$ goctl model mysql -src={patterns} -dir={dir} -cache
|
||||
```
|
||||
|
||||
help
|
||||
|
||||
```
|
||||
NAME:
|
||||
goctl model mysql ddl - generate mysql model from ddl
|
||||
|
||||
USAGE:
|
||||
goctl model mysql ddl [command options] [arguments...]
|
||||
|
||||
OPTIONS:
|
||||
--src value, -s value the path or path globbing patterns of the ddl
|
||||
--dir value, -d value the target dir
|
||||
--style value the file naming format, see [https://github.com/tal-tech/go-zero/tree/master/tools/goctl/config/readme.md]
|
||||
--cache, -c generate code with cache [optional]
|
||||
--idea for idea plugin [optional]
|
||||
```
|
||||
|
||||
* datasource
|
||||
|
||||
```shell
|
||||
$ goctl model mysql datasource -url={datasource} -table={patterns} -dir={dir} -cache=true
|
||||
```
|
||||
|
||||
help
|
||||
|
||||
```text
|
||||
NAME:
|
||||
goctl model mysql datasource - generate model from datasource
|
||||
|
||||
USAGE:
|
||||
goctl model mysql datasource [command options] [arguments...]
|
||||
|
||||
OPTIONS:
|
||||
--url value the data source of database,like "root:password@tcp(127.0.0.1:3306)/database
|
||||
--table value, -t value the table or table globbing patterns in the database
|
||||
--cache, -c generate code with cache [optional]
|
||||
--dir value, -d value the target dir
|
||||
--style value the file naming format, see [https://github.com/tal-tech/go-zero/tree/master/tools/goctl/config/readme.md]
|
||||
--idea for idea plugin [optional]
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> goctl model mysql ddl/datasource 均新增了一个`--style`参数,用于标记文件命名风格。
|
||||
|
||||
目前仅支持redis缓存,如果选择带缓存模式,即生成的`FindOne(ByXxx)`&`Delete`代码会生成带缓存逻辑的代码,目前仅支持单索引字段(除全文索引外),对于联合索引我们默认认为不需要带缓存,且不属于通用型代码,因此没有放在代码生成行列,如example中user表中的`id`、`name`、`mobile`字段均属于单字段索引。
|
||||
|
||||
* 不带缓存模式
|
||||
|
||||
* ddl
|
||||
|
||||
```shell
|
||||
$ goctl model -src={patterns} -dir={dir}
|
||||
```
|
||||
|
||||
* datasource
|
||||
|
||||
```shell
|
||||
$ goctl model mysql datasource -url={datasource} -table={patterns} -dir={dir}
|
||||
```
|
||||
|
||||
or
|
||||
* ddl
|
||||
|
||||
```shell
|
||||
$ goctl model -src={patterns} -dir={dir}
|
||||
```
|
||||
|
||||
* datasource
|
||||
|
||||
```shell
|
||||
$ goctl model mysql datasource -url={datasource} -table={patterns} -dir={dir}
|
||||
```
|
||||
|
||||
生成代码仅基本的CURD结构。
|
||||
|
||||
## 缓存
|
||||
|
||||
对于缓存这一块我选择用一问一答的形式进行罗列。我想这样能够更清晰的描述model中缓存的功能。
|
||||
|
||||
* 缓存会缓存哪些信息?
|
||||
|
||||
对于主键字段缓存,会缓存整个结构体信息,而对于单索引字段(除全文索引)则缓存主键字段值。
|
||||
|
||||
* 数据有更新(`update`)操作会清空缓存吗?
|
||||
|
||||
会,但仅清空主键缓存的信息,why?这里就不做详细赘述了。
|
||||
|
||||
* 为什么不按照单索引字段生成`updateByXxx`和`deleteByXxx`的代码?
|
||||
|
||||
理论上是没任何问题,但是我们认为,对于model层的数据操作均是以整个结构体为单位,包括查询,我不建议只查询某部分字段(不反对),否则我们的缓存就没有意义了。
|
||||
|
||||
* 为什么不支持`findPageLimit`、`findAll`这么模式代码生层?
|
||||
|
||||
目前,我认为除了基本的CURD外,其他的代码均属于<i>业务型</i>代码,这个我觉得开发人员根据业务需要进行编写更好。
|
||||
|
||||
# 类型转换规则
|
||||
| mysql dataType | golang dataType | golang dataType(if null&&default null) |
|
||||
|----------------|-----------------|----------------------------------------|
|
||||
| bool | int64 | sql.NullInt64 |
|
||||
| boolean | int64 | sql.NullInt64 |
|
||||
| tinyint | int64 | sql.NullInt64 |
|
||||
| smallint | int64 | sql.NullInt64 |
|
||||
| mediumint | int64 | sql.NullInt64 |
|
||||
| int | int64 | sql.NullInt64 |
|
||||
| integer | int64 | sql.NullInt64 |
|
||||
| bigint | int64 | sql.NullInt64 |
|
||||
| float | float64 | sql.NullFloat64 |
|
||||
| double | float64 | sql.NullFloat64 |
|
||||
| decimal | float64 | sql.NullFloat64 |
|
||||
| date | time.Time | sql.NullTime |
|
||||
| datetime | time.Time | sql.NullTime |
|
||||
| timestamp | time.Time | sql.NullTime |
|
||||
| time | string | sql.NullString |
|
||||
| year | time.Time | sql.NullInt64 |
|
||||
| char | string | sql.NullString |
|
||||
| varchar | string | sql.NullString |
|
||||
| binary | string | sql.NullString |
|
||||
| varbinary | string | sql.NullString |
|
||||
| tinytext | string | sql.NullString |
|
||||
| text | string | sql.NullString |
|
||||
| mediumtext | string | sql.NullString |
|
||||
| longtext | string | sql.NullString |
|
||||
| enum | string | sql.NullString |
|
||||
| set | string | sql.NullString |
|
||||
| json | string | sql.NullString |
|
308
go-zero.dev/cn/goctl-other.md
Normal file
308
go-zero.dev/cn/goctl-other.md
Normal file
@ -0,0 +1,308 @@
|
||||
# 其他命令
|
||||
* goctl docker
|
||||
* goctl kube
|
||||
|
||||
## goctl docker
|
||||
`goctl docker` 可以极速生成一个 Dockerfile,帮助开发/运维人员加快部署节奏,降低部署复杂度。
|
||||
|
||||
### 准备工作
|
||||
* docker安装
|
||||
|
||||
### Dockerfile 额外注意点
|
||||
* 选择最简单的镜像:比如alpine,整个镜像5M左右
|
||||
* 设置镜像时区
|
||||
```shell
|
||||
RUN apk add --no-cache tzdata
|
||||
ENV TZ Asia/Shanghai
|
||||
```
|
||||
|
||||
### 多阶段构建
|
||||
* 第一阶段构建出可执行文件,确保构建过程独立于宿主机
|
||||
* 第二阶段将第一阶段的输出作为输入,构建出最终的极简镜像
|
||||
|
||||
### Dockerfile编写过程
|
||||
* 首先安装 goctl 工具
|
||||
```shell
|
||||
$ GO111MODULE=on GOPROXY=https://goproxy.cn/,direct go get -u github.com/tal-tech/go-zero/tools/goctl
|
||||
```
|
||||
|
||||
* 在 greet 项目下创建一个 hello 服务
|
||||
```shell
|
||||
$ goctl api new hello
|
||||
```
|
||||
|
||||
文件结构如下:
|
||||
```text
|
||||
greet
|
||||
├── go.mod
|
||||
├── go.sum
|
||||
└── service
|
||||
└── hello
|
||||
├── Dockerfile
|
||||
├── etc
|
||||
│ └── hello-api.yaml
|
||||
├── hello.api
|
||||
├── hello.go
|
||||
└── internal
|
||||
├── config
|
||||
│ └── config.go
|
||||
├── handler
|
||||
│ ├── hellohandler.go
|
||||
│ └── routes.go
|
||||
├── logic
|
||||
│ └── hellologic.go
|
||||
├── svc
|
||||
│ └── servicecontext.go
|
||||
└── types
|
||||
└── types.go
|
||||
```
|
||||
* 在 `hello` 目录下一键生成 `Dockerfile`
|
||||
```shell
|
||||
$ goctl docker -go hello.go
|
||||
```
|
||||
Dockerfile 内容如下:
|
||||
```shell
|
||||
FROM golang:alpine AS builder
|
||||
LABEL stage=gobuilder
|
||||
ENV CGO_ENABLED 0
|
||||
ENV GOOS linux
|
||||
ENV GOPROXY https://goproxy.cn,direct
|
||||
WORKDIR /build/zero
|
||||
ADD go.mod .
|
||||
ADD go.sum .
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
COPY service/hello/etc /app/etc
|
||||
RUN go build -ldflags="-s -w" -o /app/hello service/hello/hello.go
|
||||
FROM alpine
|
||||
RUN apk update --no-cache
|
||||
RUN apk add --no-cache ca-certificates
|
||||
RUN apk add --no-cache tzdata
|
||||
ENV TZ Asia/Shanghai
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/hello /app/hello
|
||||
COPY --from=builder /app/etc /app/etc
|
||||
CMD ["./hello", "-f", "etc/hello-api.yaml"]
|
||||
```
|
||||
* 在 `hello` 目录下 `build` 镜像
|
||||
```shell
|
||||
$ docker build -t hello:v1 -f service/hello/Dockerfile .
|
||||
```
|
||||
|
||||
* 查看镜像
|
||||
```shell
|
||||
hello v1 5455f2eaea6b 7 minutes ago 18.1MB
|
||||
```
|
||||
|
||||
可以看出镜像大小约为18M。
|
||||
* 启动服务
|
||||
```shell
|
||||
$ docker run --rm -it -p 8888:8888 hello:v1
|
||||
```
|
||||
* 测试服务
|
||||
```shell
|
||||
$ curl -i http://localhost:8888/from/you
|
||||
```
|
||||
```text
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
Date: Thu, 10 Dec 2020 06:03:02 GMT
|
||||
Content-Length: 14
|
||||
{"message":""}
|
||||
```
|
||||
|
||||
### goctl docker总结
|
||||
goctl 工具极大简化了 Dockerfile 文件的编写,提供了开箱即用的最佳实践,并且支持了模板自定义。
|
||||
|
||||
## goctl kube
|
||||
|
||||
`goctl kube`提供了快速生成一个 `k8s` 部署文件的功能,可以加快开发/运维人员的部署进度,减少部署复杂度。
|
||||
|
||||
### 头疼编写 K8S 部署文件?
|
||||
|
||||
|
||||
- `K8S yaml` 参数很多,需要边写边查?
|
||||
- 保留回滚版本数怎么设?
|
||||
- 如何探测启动成功,如何探活?
|
||||
- 如何分配和限制资源?
|
||||
- 如何设置时区?否则打印日志是 GMT 标准时间
|
||||
- 如何暴露服务供其它服务调用?
|
||||
- 如何根据 CPU 和内存使用率来配置水平伸缩?
|
||||
|
||||
|
||||
|
||||
首先,你需要知道有这些知识点,其次要把这些知识点都搞明白也不容易,再次,每次编写依然容易出错!
|
||||
|
||||
## 创建服务镜像
|
||||
为了演示,这里我们以 `redis:6-alpine` 镜像为例。
|
||||
|
||||
## 完整 K8S 部署文件编写过程
|
||||
|
||||
- 首先安装 `goctl` 工具
|
||||
|
||||
```shell
|
||||
$ GO111MODULE=on GOPROXY=https://goproxy.cn/,direct go get -u github.com/tal-tech/go-zero/tools/goctl
|
||||
```
|
||||
|
||||
- 一键生成 K8S 部署文件
|
||||
|
||||
```shell
|
||||
$ goctl kube deploy -name redis -namespace adhoc -image redis:6-alpine -o redis.yaml -port 6379
|
||||
```
|
||||
生成的 `yaml` 文件如下:
|
||||
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: redis
|
||||
namespace: adhoc
|
||||
labels:
|
||||
app: redis
|
||||
spec:
|
||||
replicas: 3
|
||||
revisionHistoryLimit: 5
|
||||
selector:
|
||||
matchLabels:
|
||||
app: redis
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: redis
|
||||
spec:
|
||||
containers:
|
||||
- name: redis
|
||||
image: redis:6-alpine
|
||||
lifecycle:
|
||||
preStop:
|
||||
exec:
|
||||
command: ["sh","-c","sleep 5"]
|
||||
ports:
|
||||
- containerPort: 6379
|
||||
readinessProbe:
|
||||
tcpSocket:
|
||||
port: 6379
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
port: 6379
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 20
|
||||
resources:
|
||||
requests:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 1024Mi
|
||||
volumeMounts:
|
||||
- name: timezone
|
||||
mountPath: /etc/localtime
|
||||
volumes:
|
||||
- name: timezone
|
||||
hostPath:
|
||||
path: /usr/share/zoneinfo/Asia/Shanghai
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: redis-svc
|
||||
namespace: adhoc
|
||||
spec:
|
||||
ports:
|
||||
- port: 6379
|
||||
selector:
|
||||
app: redis
|
||||
---
|
||||
apiVersion: autoscaling/v2beta1
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: redis-hpa-c
|
||||
namespace: adhoc
|
||||
labels:
|
||||
app: redis-hpa-c
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: redis
|
||||
minReplicas: 3
|
||||
maxReplicas: 10
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
targetAverageUtilization: 80
|
||||
---
|
||||
apiVersion: autoscaling/v2beta1
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: redis-hpa-m
|
||||
namespace: adhoc
|
||||
labels:
|
||||
app: redis-hpa-m
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: redis
|
||||
minReplicas: 3
|
||||
maxReplicas: 10
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: memory
|
||||
targetAverageUtilization: 80
|
||||
```
|
||||
|
||||
|
||||
- 部署服务,如果 `adhoc` namespace 不存在的话,请先通过 `kubectl create namespace adhoc` 创建
|
||||
```
|
||||
$ kubectl apply -f redis.yaml
|
||||
deployment.apps/redis created
|
||||
service/redis-svc created
|
||||
horizontalpodautoscaler.autoscaling/redis-hpa-c created
|
||||
horizontalpodautoscaler.autoscaling/redis-hpa-m created
|
||||
```
|
||||
|
||||
- 查看服务允许状态
|
||||
```
|
||||
$ kubectl get all -n adhoc
|
||||
NAME READY STATUS RESTARTS AGE
|
||||
pod/redis-585bc66876-5ph26 1/1 Running 0 6m5s
|
||||
pod/redis-585bc66876-bfqxz 1/1 Running 0 6m5s
|
||||
pod/redis-585bc66876-vvfc9 1/1 Running 0 6m5s
|
||||
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
|
||||
service/redis-svc ClusterIP 172.24.15.8 <none> 6379/TCP 6m5s
|
||||
NAME READY UP-TO-DATE AVAILABLE AGE
|
||||
deployment.apps/redis 3/3 3 3 6m6s
|
||||
NAME DESIRED CURRENT READY AGE
|
||||
replicaset.apps/redis-585bc66876 3 3 3 6m6s
|
||||
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
|
||||
horizontalpodautoscaler.autoscaling/redis-hpa-c Deployment/redis 0%/80% 3 10 3 6m6s
|
||||
horizontalpodautoscaler.autoscaling/redis-hpa-m Deployment/redis 0%/80% 3 10 3 6m6s
|
||||
```
|
||||
|
||||
|
||||
- 测试服务
|
||||
```
|
||||
$ kubectl run -i --tty --rm cli --image=redis:6-alpine -n adhoc -- sh
|
||||
/data # redis-cli -h redis-svc
|
||||
redis-svc:6379> set go-zero great
|
||||
OK
|
||||
redis-svc:6379> get go-zero
|
||||
"great"
|
||||
```
|
||||
### goctl kube 总结
|
||||
`goctl` 工具极大简化了 K8S yaml 文件的编写,提供了开箱即用的最佳实践,并且支持了模板自定义。
|
||||
|
||||
# 猜你想看
|
||||
* [准备工作](prepare.md)
|
||||
* [api目录](api-dir.md)
|
||||
* [api语法](api-grammar.md)
|
||||
* [api配置](api-config.md)
|
||||
* [api命令介绍](goctl-api.md)
|
||||
* [docker介绍](https://www.docker.com)
|
||||
* [k8s介绍](https://kubernetes.io/zh/docs/home)
|
62
go-zero.dev/cn/goctl-plugin.md
Normal file
62
go-zero.dev/cn/goctl-plugin.md
Normal file
@ -0,0 +1,62 @@
|
||||
# plugin命令
|
||||
|
||||
goctl支持针对api自定义插件,那我怎么来自定义一个插件了?来看看下面最终怎么使用的一个例子。
|
||||
```go
|
||||
$ goctl api plugin -p goctl-android="android -package com.tal" -api user.api -dir .
|
||||
```
|
||||
|
||||
上面这个命令可以分解成如下几步:
|
||||
* goctl 解析api文件
|
||||
* goctl 将解析后的结构 ApiSpec 和参数传递给goctl-android可执行文件
|
||||
* goctl-android 根据 ApiSpec 结构体自定义生成逻辑。
|
||||
|
||||
此命令前面部分 goctl api plugin -p 是固定参数,goctl-android="android -package com.tal" 是plugin参数,其中goctl-android是插件二进制文件,android -package com.tal是插件的自定义参数,-api user.api -dir .是goctl通用自定义参数。
|
||||
## 怎么编写自定义插件?
|
||||
go-zero框架中包含了一个很简单的自定义插件 demo,代码如下:
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/tal-tech/go-zero/tools/goctl/plugin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
plugin, err := plugin.NewPlugin()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if plugin.Api != nil {
|
||||
fmt.Printf("api: %+v \n", plugin.Api)
|
||||
}
|
||||
fmt.Printf("dir: %s \n", plugin.Dir)
|
||||
fmt.Println("Enjoy anything you want.")
|
||||
}
|
||||
```
|
||||
|
||||
`plugin, err := plugin.NewPlugin()` 这行代码作用是解析从goctl传递过来的数据,里面包含如下部分内容:
|
||||
|
||||
```go
|
||||
type Plugin struct {
|
||||
Api *spec.ApiSpec
|
||||
Style string
|
||||
Dir string
|
||||
}
|
||||
```
|
||||
> [!TIP]
|
||||
> Api:定义了api文件的结构数据
|
||||
>
|
||||
> Style:可选参数,可以用来控制文件命名规范
|
||||
>
|
||||
> Dir:工作目录
|
||||
|
||||
|
||||
完整的基于plugin实现的android plugin演示项目
|
||||
[https://github.com/zeromicro/goctl-android](https://github.com/zeromicro/goctl-android)
|
||||
|
||||
# 猜你想看
|
||||
* [api目录](api-dir.md)
|
||||
* [api语法](api-grammar.md)
|
||||
* [api配置](api-config.md)
|
||||
* [api命令介绍](goctl-api.md)
|
@ -1,4 +1,4 @@
|
||||
# Rpc Generation
|
||||
# rpc命令
|
||||
|
||||
Goctl Rpc是`goctl`脚手架下的一个rpc服务代码生成模块,支持proto模板生成和rpc服务代码生成,通过此工具生成代码你只需要关注业务逻辑编写而不用去编写一些重复性的代码。这使得我们把精力重心放在业务上,从而加快了开发效率且降低了代码出错率。
|
||||
|
||||
@ -14,17 +14,17 @@ Goctl Rpc是`goctl`脚手架下的一个rpc服务代码生成模块,支持prot
|
||||
|
||||
### 方式一:快速生成greet服务
|
||||
|
||||
通过命令 `goctl rpc new ${servieName}`生成
|
||||
通过命令 `goctl rpc new ${servieName}`生成
|
||||
|
||||
如生成greet rpc服务:
|
||||
如生成greet rpc服务:
|
||||
|
||||
```Bash
|
||||
goctl rpc new greet
|
||||
```
|
||||
|
||||
执行后代码结构如下:
|
||||
执行后代码结构如下:
|
||||
|
||||
```golang
|
||||
```go
|
||||
.
|
||||
├── etc // yaml配置文件
|
||||
│ └── greet.yaml
|
||||
@ -69,7 +69,7 @@ if strings.ToLower(proto.Service.Name) == strings.ToLower(proto.GoPackage) {
|
||||
}
|
||||
```
|
||||
|
||||
rpc一键生成常见问题解决,见 <a href="#常见问题解决">常见问题解决</a>
|
||||
rpc一键生成常见问题解决,见[常见错误处理](error.md)
|
||||
|
||||
### 方式二:通过指定proto生成rpc服务
|
||||
|
||||
@ -79,11 +79,13 @@ rpc一键生成常见问题解决,见 <a href="#常见问题解决">常见问
|
||||
goctl rpc template -o=user.proto
|
||||
```
|
||||
|
||||
```golang
|
||||
```go
|
||||
syntax = "proto3";
|
||||
|
||||
package remote;
|
||||
|
||||
option go_package = "remote";
|
||||
|
||||
message Request {
|
||||
// 用户名
|
||||
string username = 1;
|
||||
@ -107,7 +109,7 @@ rpc一键生成常见问题解决,见 <a href="#常见问题解决">常见问
|
||||
* 生成rpc服务代码
|
||||
|
||||
```Bash
|
||||
goctl rpc proto -src=user.proto
|
||||
goctl rpc proto -src user.proto -dir .
|
||||
```
|
||||
|
||||
## 准备工作
|
||||
@ -135,6 +137,7 @@ OPTIONS:
|
||||
--src value, -s value the file path of the proto source file
|
||||
--proto_path value, -I value native command of protoc, specify the directory in which to search for imports. [optional]
|
||||
--dir value, -d value the target path of the code
|
||||
--style value the file naming format, see [https://github.com/tal-tech/go-zero/tree/master/tools/goctl/config/readme.md]
|
||||
--idea whether the command execution environment is from idea plugin. [optional]
|
||||
```
|
||||
|
||||
@ -143,6 +146,7 @@ OPTIONS:
|
||||
* --src 必填,proto数据源,目前暂时支持单个proto文件生成
|
||||
* --proto_path 可选,protoc原生子命令,用于指定proto import从何处查找,可指定多个路径,如`goctl rpc -I={path1} -I={path2} ...`,在没有import时可不填。当前proto路径不用指定,已经内置,`-I`的详细用法请参考`protoc -h`
|
||||
* --dir 可选,默认为proto文件所在目录,生成代码的目标目录
|
||||
* --style 可选,输出目录的文件命名风格,详情见https://github.com/tal-tech/go-zero/tree/master/tools/goctl/config/readme.md
|
||||
* --idea 可选,是否为idea插件中执行,终端执行可以忽略
|
||||
|
||||
|
||||
@ -156,23 +160,17 @@ OPTIONS:
|
||||
|
||||
|
||||
### 注意事项
|
||||
|
||||
* `google.golang.org/grpc`需要降级到v1.26.0,且protoc-gen-go版本不能高于v1.3.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)
|
69
go-zero.dev/cn/goctl.md
Normal file
69
go-zero.dev/cn/goctl.md
Normal file
@ -0,0 +1,69 @@
|
||||
# Goctl
|
||||
|
||||
goctl是go-zero微服务框架下的代码生成工具。使用 goctl 可显著提升开发效率,让开发人员将时间重点放在业务开发上,其功能有:
|
||||
|
||||
- api服务生成
|
||||
- rpc服务生成
|
||||
- model代码生成
|
||||
- 模板管理
|
||||
|
||||
本节将包含以下内容:
|
||||
|
||||
* [命令大全](goctl-commands.md)
|
||||
* [api命令](goctl-api.md)
|
||||
* [rpc命令](goctl-rpc.md)
|
||||
* [model命令](goctl-model.md)
|
||||
* [plugin命令](goctl-plugin.md)
|
||||
* [其他命令](goctl-other.md)
|
||||
|
||||
## goctl 读音
|
||||
很多人会把 `goctl` 读作 `go-C-T-L`,这种是错误的念法,应参照 `go control` 读做 `ɡō kənˈtrōl`。
|
||||
|
||||
## 查看版本信息
|
||||
```shell
|
||||
$ goctl -v
|
||||
```
|
||||
|
||||
如果安装了goctl则会输出以下格式的文本信息:
|
||||
|
||||
```text
|
||||
goctl version ${version} ${os}/${arch}
|
||||
```
|
||||
|
||||
例如输出:
|
||||
```text
|
||||
goctl version 1.1.5 darwin/amd64
|
||||
```
|
||||
|
||||
版本号说明
|
||||
* 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的环境变量中。
|
53
go-zero.dev/cn/golang-install.md
Normal file
53
go-zero.dev/cn/golang-install.md
Normal file
@ -0,0 +1,53 @@
|
||||
# Golang环境安装
|
||||
|
||||
## 前言
|
||||
开发golang程序,必然少不了对其环境的安装,我们这里选择以1.15.1为例。
|
||||
|
||||
## 官方文档
|
||||
[https://golang.google.cn/doc/install](https://golang.google.cn/doc/install)
|
||||
|
||||
## mac OS安装Go
|
||||
|
||||
* 下载并安装[Go for Mac](https://dl.google.com/go/go1.15.1.darwin-amd64.pkg)
|
||||
* 验证安装结果
|
||||
```shell
|
||||
$ go version
|
||||
```
|
||||
```text
|
||||
go version go1.15.1 darwin/amd64
|
||||
```
|
||||
## linux 安装Go
|
||||
* 下载[Go for Linux](https://golang.org/dl/go1.15.8.linux-amd64.tar.gz)
|
||||
* 解压压缩包至`/usr/local`
|
||||
```shell
|
||||
$ tar -C /usr/local -xzf go1.15.8.linux-amd64.tar.gz
|
||||
```
|
||||
* 添加`/usr/local/go/bin`到环境变量
|
||||
```shell
|
||||
$ $HOME/.profile
|
||||
```
|
||||
```shell
|
||||
export PATH=$PATH:/usr/local/go/bin
|
||||
```
|
||||
```shell
|
||||
$ source $HOME/.profile
|
||||
```
|
||||
* 验证安装结果
|
||||
```shell
|
||||
$ go version
|
||||
```
|
||||
```text
|
||||
go version go1.15.1 linux/amd64
|
||||
```
|
||||
## Windows安装Go
|
||||
* 下载并安装[Go for Windows](https://golang.org/dl/go1.15.8.windows-amd64.msi)
|
||||
* 验证安装结果
|
||||
```shell
|
||||
$ go version
|
||||
```
|
||||
```text
|
||||
go version go1.15.1 windows/amd64
|
||||
```
|
||||
|
||||
## 其他
|
||||
更多操作系统安装见[https://golang.org/dl/](https://golang.org/dl/)
|
37
go-zero.dev/cn/gomod-config.md
Normal file
37
go-zero.dev/cn/gomod-config.md
Normal file
@ -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)
|
7
go-zero.dev/cn/goreading.md
Normal file
7
go-zero.dev/cn/goreading.md
Normal file
@ -0,0 +1,7 @@
|
||||
# Go夜读
|
||||
|
||||
* [2020-08-16 晓黑板 go-zero 微服务框架的架构设计](https://talkgo.org/t/topic/729)
|
||||
* [2020-10-03 go-zero 微服务框架和线上交流](https://talkgo.org/t/topic/1070)
|
||||
* [防止缓存击穿之进程内共享调用](https://talkgo.org/t/topic/968)
|
||||
* [基于go-zero实现JWT认证](https://talkgo.org/t/topic/1114)
|
||||
* [再见go-micro!企业项目迁移go-zero全攻略(一)](https://talkgo.org/t/topic/1607)
|
3
go-zero.dev/cn/gotalk.md
Normal file
3
go-zero.dev/cn/gotalk.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Go开源说
|
||||
|
||||
* [Go 开源说第四期 - Go-Zero](https://www.bilibili.com/video/BV1Jy4y127Xu)
|
114
go-zero.dev/cn/intellij.md
Normal file
114
go-zero.dev/cn/intellij.md
Normal file
@ -0,0 +1,114 @@
|
||||
# intellij插件
|
||||
|
||||
## Go-Zero Plugin
|
||||
|
||||
[<img src="https://img.shields.io/badge/Github-go--zero-brightgreen?logo=github" alt="go-zero"/>](https://github.com/tal-tech/go-zero)
|
||||
[<img src="https://img.shields.io/badge/License-MIT-blue" alt="license"/>](https://github.com/zeromicro/goctl-intellij/blob/main/LICENSE)
|
||||
[<img src="https://img.shields.io/badge/Release-0.7.14-red" alt="release"/>](https://github.com/zeromicro/goctl-intellij/releases)
|
||||
[<img src="https://github.com/zeromicro/goctl-intellij/workflows/Java%20CI%20with%20Gradle/badge.svg" alt="Java CI with Gradle" />](https://github.com/zeromicro/goctl-intellij/actions)
|
||||
|
||||
## 介绍
|
||||
一款支持go-zero api语言结构语法高亮、检测以及api、rpc、model快捷生成的插件工具。
|
||||
|
||||
|
||||
## idea版本要求
|
||||
* IntelliJ 2019.3+ (Ultimate or Community)
|
||||
* Goland 2019.3+
|
||||
* WebStorm 2019.3+
|
||||
* PhpStorm 2019.3+
|
||||
* PyCharm 2019.3+
|
||||
* RubyMine 2019.3+
|
||||
* CLion 2019.3+
|
||||
|
||||
## 版本特性
|
||||
* api语法高亮
|
||||
* api语法、语义检测
|
||||
* struct、route、handler重复定义检测
|
||||
* type跳转到类型声明位置
|
||||
* 上下文菜单中支持api、rpc、mode相关menu选项
|
||||
* 代码格式化(option+command+L)
|
||||
* 代码提示
|
||||
|
||||
## 安装方式
|
||||
|
||||
### 方式一
|
||||
在github的release中找到最新的zip包,下载本地安装即可。(无需解压)
|
||||
|
||||
### 方式二
|
||||
在plugin商店中,搜索`Goctl`安装即可
|
||||
|
||||
|
||||
## 预览
|
||||
![preview](./resource/api-compare.png)
|
||||
|
||||
## 新建 Api(Proto) file
|
||||
在工程区域目标文件夹`右键->New-> New Api(Proto) File ->Empty File/Api(Proto) Template`,如图:
|
||||
![preview](./resource/api-new.png)
|
||||
|
||||
# 快速生成api/rpc服务
|
||||
在目标文件夹`右键->New->Go Zero -> Api Greet Service/Rpc Greet Service`
|
||||
|
||||
![preview](./resource/service.png)
|
||||
|
||||
# Api/Rpc/Model Code生成
|
||||
|
||||
## 方法一(工程区域)
|
||||
|
||||
对应文件(api、proto、sql)`右键->New->Go Zero-> Api/Rpc/Model Code`,如图:
|
||||
|
||||
![preview](./resource/project_generate_code.png)
|
||||
|
||||
## 方法二(编辑区域)
|
||||
对应文件(api、proto、sql)`右键-> Generate-> Api/Rpc/Model Code`
|
||||
|
||||
|
||||
# 错误提示
|
||||
![context menu](./resource/alert.png)
|
||||
|
||||
|
||||
# Live Template
|
||||
Live Template可以加快我们对api文件的编写,比如我们在go文件中输入`main`关键字根据tip回车后会插入一段模板代码
|
||||
```go
|
||||
func main(){
|
||||
|
||||
}
|
||||
```
|
||||
或者说看到下图你会更加熟悉,曾几何时你还在这里定义过template
|
||||
![context menu](./resource/go_live_template.png)
|
||||
|
||||
下面就进入今天api语法中的模板使用说明吧,我们先来看看service模板的效果
|
||||
![context menu](./resource/live_template.gif)
|
||||
|
||||
首先上一张图了解一下api文件中几个模板生效区域(psiTree元素区域)
|
||||
![context menu](./resource/psiTree.png)
|
||||
|
||||
#### 预设模板及生效区域
|
||||
| 模板关键字 | psiTree生效区域 |描述
|
||||
| ---- | ---- | ---- |
|
||||
| @doc | ApiService |doc注释模板|
|
||||
| doc | ApiService |doc注释模板|
|
||||
| struct | Struct |struct声明模板|
|
||||
| info | ApiFile |info block模板|
|
||||
| type | ApiFile |type group模板|
|
||||
| handler | ApiService |handler文件名模板|
|
||||
| get | ApiService |get方法路由模板|
|
||||
| head | ApiService |head方法路由模板|
|
||||
| post | ApiService |post方法路由模板|
|
||||
| put | ApiService |put方法路由模板|
|
||||
| delete | ApiService |delete方法路由模板|
|
||||
| connect | ApiService |connect方法路由模板|
|
||||
| options | ApiService |options方法路由模板|
|
||||
| trace | ApiService |trace方法路由模板|
|
||||
| service | ApiFile |service服务block模板|
|
||||
| json | Tag、Tag literal |tag模板|
|
||||
| xml | Tag、Tag literal |tag模板|
|
||||
| path | Tag、Tag literal |tag模板|
|
||||
| form | Tag、Tag literal |tag模板|
|
||||
|
||||
关于每个模板对应内容可在`Goland(mac Os)->Preference->Editor->Live Templates-> Api|Api Tags`中查看详细模板内容,如json tag模板内容为
|
||||
```go
|
||||
json:"$FIELD_NAME$"
|
||||
```
|
||||
![context menu](./resource/json_tag.png)
|
||||
|
||||
|
53
go-zero.dev/cn/join-us.md
Normal file
53
go-zero.dev/cn/join-us.md
Normal file
@ -0,0 +1,53 @@
|
||||
# 加入我们
|
||||
|
||||
## 概要
|
||||
<img src="./resource/go-zero-logo.png" alt="go-zero" width="100px" height="100px" align="right" />
|
||||
|
||||
[go-zero](https://github.com/tal-tech/go-zero) 是一个基于[MIT License](https://github.com/tal-tech/go-zero/blob/master/LICENSE) 的开源项目,大家在使用中发现bug,有新的特性等,均可以参与到go-zero的贡献中来,我们非常欢迎大家的积极参与,也会最快响应大家提出的各种问题,pr等。
|
||||
|
||||
## 贡献形式
|
||||
* [Pull Request](https://github.com/tal-tech/go-zero/pulls)
|
||||
* [Issue](https://github.com/tal-tech/go-zero/issues)
|
||||
|
||||
## 贡献须知
|
||||
go-zero 的Pull request中的代码需要满足一定规范
|
||||
* 命名规范,请阅读[命名规范](naming-spec.md)
|
||||
* 以英文注释为主
|
||||
* pr时备注好功能特性,描述需要清晰,简洁
|
||||
* 增加单元测试覆盖率达80%+
|
||||
|
||||
## 贡献代码(pr)
|
||||
* 进入[go-zero](https://github.com/tal-tech/go-zero) 项目,fork一份[go-zero](https://github.com/tal-tech/go-zero) 项目到自己的github仓库中。
|
||||
* 回到自己的github主页,找到`xx/go-zero`项目,其中xx为你的用户名,如`anqiansong/go-zero`
|
||||
|
||||
![fork](./resource/fork.png)
|
||||
* 克隆代码到本地
|
||||
|
||||
![clone](./resource/clone.png)
|
||||
* 开发代码,push到自己的github仓库
|
||||
* 进入自己的github中go-zero项目,点击浮层上的的`【Pull requests】`进入Compare页面。
|
||||
|
||||
![pr](./resource/new_pr.png)
|
||||
|
||||
* `base repository`选择`tal-tech/go-zero` `base:master`,`head repository`选择`xx/go-zero` `compare:$branch` ,`$branch`为你开发的分支,如图:
|
||||
|
||||
![pr](./resource/compare.png)
|
||||
|
||||
* 点击`【Create pull request】`即可实现pr申请
|
||||
* 确认pr是否提交成功,进入[go-zero](https://github.com/tal-tech/go-zero) 的[Pull requests](https://github.com/tal-tech/go-zero/pulls) 查看,应该有自己提交的记录,名称为你的开发时的分支名称
|
||||
|
||||
![pr record](./resource/pr_record.png)
|
||||
|
||||
## Issue
|
||||
在我们的社区中,有很多伙伴会积极的反馈一些go-zero使用过程中遇到的问题,由于社区人数较多,我们虽然会实时的关注社区动态,但大家问题反馈过来都是随机的,当我们团队还在解决某一个伙伴提出的问题时,另外的问题也反馈上来,可能会导致团队会很容易忽略掉,为了能够一一的解决大家的问题,我们强烈建议大家通过issue的方式来反馈问题,包括但不限于bug,期望的新功能特性等,我们在实现某一个新特性时也会在issue中体现,大家在这里也能够在这里获取到go-zero的最新动向,也欢迎大家来积极的参与讨论。
|
||||
|
||||
### 怎么提Issue
|
||||
* 点击[这里](https://github.com/tal-tech/go-zero/issues) 进入go-zero的Issue页面或者直接访问[https://github.com/tal-tech/go-zero/issues](https://github.com/tal-tech/go-zero/issues) 地址
|
||||
* 点击右上角的`【New issue】`新建issue
|
||||
* 填写issue标题和内容
|
||||
* 点击`【Submit new issue】`提交issue
|
||||
|
||||
|
||||
## 参考文档
|
||||
|
||||
* [Github Pull request](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/proposing-changes-to-your-work-with-pull-requests)
|
238
go-zero.dev/cn/jwt.md
Normal file
238
go-zero.dev/cn/jwt.md
Normal file
@ -0,0 +1,238 @@
|
||||
# jwt鉴权
|
||||
|
||||
## 概述
|
||||
> JSON Web令牌(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑而独立的方法,用于在各方之间安全地将信息作为JSON对象传输。由于此信息是经过数字签名的,因此可以被验证和信任。可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对对JWT进行签名。
|
||||
|
||||
## 什么时候应该使用JWT
|
||||
* 授权:这是使用JWT的最常见方案。一旦用户登录,每个后续请求将包括JWT,从而允许用户访问该令牌允许的路由,服务和资源。单一登录是当今广泛使用JWT的一项功能,因为它的开销很小并且可以在不同的域中轻松使用。
|
||||
|
||||
* 信息交换:JSON Web令牌是在各方之间安全地传输信息的一种好方法。因为可以对JWT进行签名(例如,使用公钥/私钥对),所以您可以确保发件人是他们所说的人。此外,由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否未被篡改。
|
||||
|
||||
## 为什么要使用JSON Web令牌
|
||||
由于JSON不如XML冗长,因此在编码时JSON的大小也较小,从而使JWT比SAML更为紧凑。这使得JWT是在HTML和HTTP环境中传递的不错的选择。
|
||||
|
||||
在安全方面,只能使用HMAC算法由共享机密对SWT进行对称签名。但是,JWT和SAML令牌可以使用X.509证书形式的公用/专用密钥对进行签名。与签署JSON的简单性相比,使用XML Digital Signature签署XML而不引入模糊的安全漏洞是非常困难的。
|
||||
|
||||
JSON解析器在大多数编程语言中都很常见,因为它们直接映射到对象。相反,XML没有自然的文档到对象的映射。与SAML断言相比,这使使用JWT更加容易。
|
||||
|
||||
关于用法,JWT是在Internet规模上使用的。这突显了在多个平台(尤其是移动平台)上对JSON Web令牌进行客户端处理的简便性。
|
||||
|
||||
> [!TIP]
|
||||
> 以上内容全部来自[jwt官网介绍](https://jwt.io/introduction)
|
||||
|
||||
## go-zero中怎么使用jwt
|
||||
jwt鉴权一般在api层使用,我们这次演示工程中分别在user api登录时生成jwt token,在search api查询图书时验证用户jwt token两步来实现。
|
||||
|
||||
### user api生成jwt token
|
||||
接着[业务编码](business-coding.md)章节的内容,我们完善上一节遗留的`getJwtToken`方法,即生成jwt token逻辑
|
||||
|
||||
#### 添加配置定义和yaml配置项
|
||||
```shell
|
||||
$ vim service/user/cmd/api/internal/config/config.go
|
||||
```
|
||||
```go
|
||||
type Config struct {
|
||||
rest.RestConf
|
||||
Mysql struct{
|
||||
DataSource string
|
||||
}
|
||||
CacheRedis cache.CacheConf
|
||||
Auth struct {
|
||||
AccessSecret string
|
||||
AccessExpire int64
|
||||
}
|
||||
}
|
||||
```
|
||||
```shell
|
||||
$ vim service/user/cmd/api/etc/user-api.yaml
|
||||
```
|
||||
```yaml
|
||||
Name: user-api
|
||||
Host: 0.0.0.0
|
||||
Port: 8888
|
||||
Mysql:
|
||||
DataSource: $user:$password@tcp($url)/$db?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai
|
||||
CacheRedis:
|
||||
- Host: $host
|
||||
Pass: $pass
|
||||
Type: node
|
||||
Auth:
|
||||
AccessSecret: $AccessSecret
|
||||
AccessExpire: $AccessExpire
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> $AccessSecret:生成jwt token的密钥,最简单的方式可以使用一个uuid值。
|
||||
>
|
||||
> $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)
|
5
go-zero.dev/cn/learning-resource.md
Normal file
5
go-zero.dev/cn/learning-resource.md
Normal file
@ -0,0 +1,5 @@
|
||||
# 学习资源
|
||||
这里将不定期更新go-zero的最新学习资源通道,目前包含通道有:
|
||||
* [公众号](wechat.md)
|
||||
* [Go夜读](goreading.md)
|
||||
* [Go开源说](gotalk.md)
|
139
go-zero.dev/cn/log-collection.md
Normal file
139
go-zero.dev/cn/log-collection.md
Normal file
@ -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)
|
185
go-zero.dev/cn/logx.md
Normal file
185
go-zero.dev/cn/logx.md
Normal file
@ -0,0 +1,185 @@
|
||||
# logx
|
||||
|
||||
|
||||
## 使用示例
|
||||
|
||||
```go
|
||||
var c logx.LogConf
|
||||
// 从 yaml 文件中 初始化配置
|
||||
conf.MustLoad("config.yaml", &c)
|
||||
|
||||
// logx 根据配置初始化
|
||||
logx.MustSetup(c)
|
||||
|
||||
logx.Info("This is info!")
|
||||
logx.Infof("This is %s!", "info")
|
||||
|
||||
logx.Error("This is error!")
|
||||
logx.Errorf("this is %s!", "error")
|
||||
|
||||
logx.Close()
|
||||
```
|
||||
|
||||
## 初始化
|
||||
logx 有很多可以配置项,可以参考 logx.LogConf 中的定义。目前可以使用
|
||||
|
||||
```go
|
||||
logx.MustSetUp(c)
|
||||
```
|
||||
进行初始化配置,如果没有进行初始化配置,所有的配置将使用默认配置。
|
||||
|
||||
## Level
|
||||
logx 支持的打印日志级别有:
|
||||
- info
|
||||
- error
|
||||
- server
|
||||
- fatal
|
||||
- slow
|
||||
- stat
|
||||
|
||||
可以使用对应的方法打印出对应级别的日志。
|
||||
同时为了方便调试,线上使用,可以动态调整日志打印级别,其中可以通过 **logx.SetLevel(uint32)** 进行级别设置,也可以通过配置初始化进行设置。目前支持的参数为:
|
||||
|
||||
```go
|
||||
const (
|
||||
// 打印所有级别的日志
|
||||
InfoLevel = iotas
|
||||
// 打印 errors, slows, stacks 日志
|
||||
ErrorLevel
|
||||
// 仅打印 server 级别日志
|
||||
SevereLevel
|
||||
)
|
||||
```
|
||||
|
||||
## 日志模式
|
||||
目前日志打印模式主要分为2种,一种文件输出,一种控制台输出。推荐方式,当采用 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 毫秒。
|
309
go-zero.dev/cn/micro-service.md
Normal file
309
go-zero.dev/cn/micro-service.md
Normal file
@ -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)
|
124
go-zero.dev/cn/middleware.md
Normal file
124
go-zero.dev/cn/middleware.md
Normal file
@ -0,0 +1,124 @@
|
||||
# 中间件使用
|
||||
在上一节,我们演示了怎么使用jwt鉴权,相信你已经掌握了对jwt的基本使用,本节我们来看一下api服务中间件怎么使用。
|
||||
|
||||
## 中间件分类
|
||||
在go-zero中,中间件可以分为路由中间件和全局中间件,路由中间件是指某一些特定路由需要实现中间件逻辑,其和jwt类似,没有放在`jwt:xxx`下的路由不会使用中间件功能,
|
||||
而全局中间件的服务范围则是整个服务。
|
||||
|
||||
## 中间件使用
|
||||
这里以`search`服务为例来演示中间件的使用
|
||||
|
||||
### 路由中间件
|
||||
* 重新编写`search.api`文件,添加`middleware`声明
|
||||
```shell
|
||||
$ cd service/search/cmd/api
|
||||
$ vim search.api
|
||||
```
|
||||
```text
|
||||
type SearchReq struct {}
|
||||
|
||||
type SearchReply struct {}
|
||||
|
||||
@server(
|
||||
jwt: Auth
|
||||
middleware: Example // 路由中间件声明
|
||||
)
|
||||
service search-api {
|
||||
@handler search
|
||||
get /search/do (SearchReq) returns (SearchReply)
|
||||
}
|
||||
```
|
||||
* 重新生成api代码
|
||||
```shell
|
||||
$ goctl api go -api search.api -dir .
|
||||
```
|
||||
```text
|
||||
etc/search-api.yaml exists, ignored generation
|
||||
internal/config/config.go exists, ignored generation
|
||||
search.go exists, ignored generation
|
||||
internal/svc/servicecontext.go exists, ignored generation
|
||||
internal/handler/searchhandler.go exists, ignored generation
|
||||
internal/handler/pinghandler.go exists, ignored generation
|
||||
internal/logic/searchlogic.go exists, ignored generation
|
||||
internal/logic/pinglogic.go exists, ignored generation
|
||||
Done.
|
||||
```
|
||||
生成完后会在`internal`目录下多一个`middleware`的目录,这里即中间件文件,后续中间件的实现逻辑也在这里编写。
|
||||
* 完善资源依赖`ServiceContext`
|
||||
```shell
|
||||
$ vim service/search/cmd/api/internal/svc/servicecontext.go
|
||||
```
|
||||
```go
|
||||
type ServiceContext struct {
|
||||
Config config.Config
|
||||
Example rest.Middleware
|
||||
}
|
||||
|
||||
func NewServiceContext(c config.Config) *ServiceContext {
|
||||
return &ServiceContext{
|
||||
Config: c,
|
||||
Example: middleware.NewExampleMiddleware().Handle,
|
||||
}
|
||||
}
|
||||
```
|
||||
* 编写中间件逻辑
|
||||
这里仅添加一行日志,内容example middle,如果服务运行输出example middle则代表中间件使用起来了。
|
||||
|
||||
```shell
|
||||
$ vim service/search/cmd/api/internal/middleware/examplemiddleware.go
|
||||
```
|
||||
```go
|
||||
package middleware
|
||||
|
||||
import "net/http"
|
||||
|
||||
type ExampleMiddleware struct {
|
||||
}
|
||||
|
||||
func NewExampleMiddleware() *ExampleMiddleware {
|
||||
return &ExampleMiddleware{}
|
||||
}
|
||||
|
||||
func (m *ExampleMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO generate middleware implement function, delete after code implementation
|
||||
|
||||
// Passthrough to next handler if need
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
```
|
||||
* 启动服务验证
|
||||
```text
|
||||
{"@timestamp":"2021-02-09T11:32:57.931+08","level":"info","content":"example middle"}
|
||||
```
|
||||
|
||||
### 全局中间件
|
||||
通过rest.Server提供的Use方法即可
|
||||
```go
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
var c config.Config
|
||||
conf.MustLoad(*configFile, &c)
|
||||
|
||||
ctx := svc.NewServiceContext(c)
|
||||
server := rest.MustNewServer(c.RestConf)
|
||||
defer server.Stop()
|
||||
|
||||
// 全局中间件
|
||||
server.Use(func(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
logx.Info("global middleware")
|
||||
next(w, r)
|
||||
}
|
||||
})
|
||||
handler.RegisterHandlers(server, ctx)
|
||||
|
||||
fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port)
|
||||
server.Start()
|
||||
}
|
||||
```
|
||||
```text
|
||||
{"@timestamp":"2021-02-09T11:50:15.388+08","level":"info","content":"global middleware"}
|
||||
```
|
55
go-zero.dev/cn/model-gen.md
Normal file
55
go-zero.dev/cn/model-gen.md
Normal file
@ -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)
|
90
go-zero.dev/cn/monolithic-service.md
Normal file
90
go-zero.dev/cn/monolithic-service.md
Normal file
@ -0,0 +1,90 @@
|
||||
# 单体服务
|
||||
|
||||
## 前言
|
||||
由于go-zero集成了web/rpc于一体,社区有部分小伙伴会问我,go-zero的定位是否是一款微服务框架,
|
||||
答案是否定的,go-zero虽然集众多功能于一身,但你可以将其中任何一个功能独立出来去单独使用,也可以开发单体服务,
|
||||
不是说每个服务上来就一定要采用微服务的架构的设计,这点大家可以看看作者(kevin)的第四期[开源说](https://www.bilibili.com/video/BV1Jy4y127Xu) ,其中对此有详细的讲解。
|
||||
|
||||
## 创建greet服务
|
||||
```shell
|
||||
$ cd ~/go-zero-demo
|
||||
$ goctl api new greet
|
||||
Done.
|
||||
```
|
||||
|
||||
查看一下`greet`服务的结构
|
||||
```shell
|
||||
$ cd greet
|
||||
$ tree
|
||||
```
|
||||
```text
|
||||
.
|
||||
├── etc
|
||||
│ └── greet-api.yaml
|
||||
├── greet.api
|
||||
├── greet.go
|
||||
└── internal
|
||||
├── config
|
||||
│ └── config.go
|
||||
├── handler
|
||||
│ ├── greethandler.go
|
||||
│ └── routes.go
|
||||
├── logic
|
||||
│ └── greetlogic.go
|
||||
├── svc
|
||||
│ └── servicecontext.go
|
||||
└── types
|
||||
└── types.go
|
||||
```
|
||||
由以上目录结构可以观察到,`greet`服务虽小,但"五脏俱全"。接下来我们就可以在`greetlogic.go`中编写业务代码了。
|
||||
|
||||
## 编写逻辑
|
||||
```shell
|
||||
$ vim ~/go-zero-demo/greet/internal/logic/greetlogic.go
|
||||
```
|
||||
```go
|
||||
func (l *GreetLogic) Greet(req types.Request) (*types.Response, error) {
|
||||
return &types.Response{
|
||||
Message: "Hello go-zero",
|
||||
}, nil
|
||||
}
|
||||
```
|
||||
|
||||
## 启动并访问服务
|
||||
|
||||
* 启动服务
|
||||
```shell
|
||||
$ cd ~/go-zero-demo/greet
|
||||
$ go run greet.go -f etc/greet-api.yaml
|
||||
```
|
||||
```text
|
||||
Starting server at 0.0.0.0:8888...
|
||||
```
|
||||
|
||||
* 访问服务
|
||||
```shell
|
||||
$ curl -i -X GET \
|
||||
http://localhost:8888/from/you
|
||||
```
|
||||
|
||||
```text
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
Date: Sun, 07 Feb 2021 04:31:25 GMT
|
||||
Content-Length: 27
|
||||
|
||||
{"message":"Hello go-zero"}
|
||||
```
|
||||
|
||||
# 源码
|
||||
[greet源码](https://github.com/zeromicro/go-zero-demo/tree/master/greet)
|
||||
|
||||
# 猜你想看
|
||||
* [goctl使用说明](goctl.md)
|
||||
* [api目录结构介绍](api-dir.md)
|
||||
* [api语法](api-grammar.md)
|
||||
* [api配置文件介绍](api-config.md)
|
||||
* [api中间件使用](middleware.md)
|
||||
|
||||
|
||||
|
180
go-zero.dev/cn/mysql.md
Normal file
180
go-zero.dev/cn/mysql.md
Normal file
@ -0,0 +1,180 @@
|
||||
# mysql
|
||||
|
||||
`go-zero` 提供更易于操作的 `mysql` API。
|
||||
|
||||
> [!TIP]
|
||||
> 但是 `stores/mysql` 定位不是一个 `orm` 框架,如果你需要通过 `sql/scheme` -> `model/struct` 逆向生成 `model` 层代码,可以使用「[goctl model](https://go-zero.dev/cn/goctl-model.html)」,这个是极好的功能。
|
||||
|
||||
|
||||
|
||||
## Feature
|
||||
|
||||
- 相比原生,提供对开发者更友好的 API
|
||||
- 完成 `queryField -> struct` 的自动赋值
|
||||
- 批量插入「bulkinserter」
|
||||
- 自带熔断
|
||||
- API 经过若干个服务的不断考验
|
||||
- 提供 `partial assignment` 特性,不强制 `struct` 的严格赋值
|
||||
|
||||
|
||||
|
||||
## Connection
|
||||
下面用一个例子简单说明一下如何创建一个 `mysql` 连接的 model:
|
||||
```go
|
||||
// 1. 快速连接一个 mysql
|
||||
// datasource: mysql dsn
|
||||
heraMysql := sqlx.NewMysql(datasource)
|
||||
|
||||
// 2. 在 servicecontext 中调用,懂model上层的logic层调用
|
||||
model.NewMysqlModel(heraMysql, tablename),
|
||||
|
||||
// 3. model层 mysql operation
|
||||
func NewMysqlModel(conn sqlx.SqlConn, table string) *MysqlModel {
|
||||
defer func() {
|
||||
recover()
|
||||
}()
|
||||
// 4. 创建一个批量insert的 [mysql executor]
|
||||
// conn: mysql connection; insertsql: mysql insert sql
|
||||
bulkInserter , err := sqlx.NewBulkInserter(conn, insertsql)
|
||||
if err != nil {
|
||||
logx.Error("Init bulkInsert Faild")
|
||||
panic("Init bulkInsert Faild")
|
||||
return nil
|
||||
}
|
||||
return &MysqlModel{conn: conn, table: table, Bulk: bulkInserter}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## CRUD
|
||||
|
||||
准备一个 `User model`
|
||||
```go
|
||||
var userBuilderQueryRows = strings.Join(builderx.FieldNames(&User{}), ",")
|
||||
|
||||
type User struct {
|
||||
Avatar string `db:"avatar"` // 头像
|
||||
UserName string `db:"user_name"` // 姓名
|
||||
Sex int `db:"sex"` // 1男,2女
|
||||
MobilePhone string `db:"mobile_phone"` // 手机号
|
||||
}
|
||||
```
|
||||
其中 `userBuilderQueryRows` : `go-zero` 中提供 `struct -> [field...]` 的转化,开发者可以将此当成模版直接使用。
|
||||
### insert
|
||||
```go
|
||||
// 一个实际的insert model层操作
|
||||
func (um *UserModel) Insert(user *User) (int64, error) {
|
||||
const insertsql = `insert into `+um.table+` (`+userBuilderQueryRows+`) values(?, ?, ?)`
|
||||
// insert op
|
||||
res, err := um.conn.Exec(insertsql, user.Avatar, user.UserName, user.Sex, user.MobilePhone)
|
||||
if err != nil {
|
||||
logx.Errorf("insert User Position Model Model err, err=%v", err)
|
||||
return -1, err
|
||||
}
|
||||
id, err := res.LastInsertId()
|
||||
if err != nil {
|
||||
logx.Errorf("insert User Model to Id parse id err,err=%v", err)
|
||||
return -1, err
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
```
|
||||
|
||||
- 拼接 `insertsql`
|
||||
- 将 `insertsql` 以及占位符对应的 `struct field` 传入 -> `con.Exex(insertsql, field...)`
|
||||
|
||||
|
||||
> [!WARNING]
|
||||
> `conn.Exec(sql, args...)` : `args...` 需对应 `sql` 中的占位符。不然会出现赋值异常的问题。
|
||||
|
||||
|
||||
`go-zero` 将涉及 `mysql` 修改的操作统一抽象为 `Exec()` 。所以 `insert/update/delete` 操作本质上是一致的。其余两个操作,开发者按照上述 `insert` 流程尝试即可。
|
||||
|
||||
|
||||
### query
|
||||
|
||||
|
||||
只需要传入 `querysql` 和 `model` 结构体,就可以获取到被赋值好的 `model` 。无需开发者手动赋值。
|
||||
```go
|
||||
func (um *UserModel) FindOne(uid int64) (*User, error) {
|
||||
var user User
|
||||
const querysql = `select `+userBuilderQueryRows+` from `+um.table+` where id=? limit 1`
|
||||
err := um.conn.QueryRow(&user, querysql, uid)
|
||||
if err != nil {
|
||||
logx.Errorf("userId.findOne error, id=%d, err=%s", uid, err.Error())
|
||||
if err == sqlx.ErrNotFound {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
```
|
||||
|
||||
- 声明 `model struct` ,拼接 `querysql`
|
||||
- `conn.QueryRow(&model, querysql, args...)` : `args...` 与 `querysql` 中的占位符对应。
|
||||
|
||||
|
||||
|
||||
> [!WARNING]
|
||||
> `QueryRow()` 中第一个参数需要传入 `Ptr` 「底层需要反射对 `struct` 进行赋值」
|
||||
|
||||
上述是查询一条记录,如果需要查询多条记录时,可以使用 `conn.QueryRows()`
|
||||
```go
|
||||
func (um *UserModel) FindOne(sex int) ([]*User, error) {
|
||||
users := make([]*User, 0)
|
||||
const querysql = `select `+userBuilderQueryRows+` from `+um.table+` where sex=?`
|
||||
err := um.conn.QueryRows(&users, querysql, sex)
|
||||
if err != nil {
|
||||
logx.Errorf("usersSex.findOne error, sex=%d, err=%s", uid, err.Error())
|
||||
if err == sqlx.ErrNotFound {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
```
|
||||
与 `QueryRow()` 不同的地方在于: `model` 需要设置成 `Slice` ,因为是查询多行,需要对多个 `model` 赋值。但同时需要注意️:第一个参数需要传入 `Ptr`
|
||||
|
||||
### querypartial
|
||||
|
||||
|
||||
从使用上,与上述的 `QueryRow()` 无异「这正体现了 `go-zero` 高度的抽象设计」。
|
||||
|
||||
|
||||
区别:
|
||||
|
||||
- `QueryRow()` : `len(querysql fields) == len(struct)` ,且一一对应
|
||||
- `QueryRowPartial()` :`len(querysql fields) <= len(struct)`
|
||||
|
||||
|
||||
|
||||
numA:数据库字段数;numB:定义的 `struct` 属性数。
|
||||
如果 `numA < numB` ,但是你恰恰又需要统一多处的查询时「定义了多个 `struct` 返回不同的用途,恰恰都可以使用相同的 `querysql` 」,就可以使用 `QueryRowPartial()`
|
||||
|
||||
|
||||
## 事务
|
||||
|
||||
|
||||
要在事务中执行一系列操作,一般流程如下:
|
||||
```go
|
||||
var insertsql = `insert into User(uid, username, mobilephone) values (?, ?, ?)`
|
||||
err := usermodel.conn.Transact(func(session sqlx.Session) error {
|
||||
stmt, err := session.Prepare(insertsql)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
// 返回任何错误都会回滚事务
|
||||
if _, err := stmt.Exec(uid, username, mobilephone); err != nil {
|
||||
logx.Errorf("insert userinfo stmt exec: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 还可以继续执行 insert/update/delete 相关操作
|
||||
return nil
|
||||
})
|
||||
```
|
||||
如同上述例子,开发者只需将 **事务** 中的操作都包装在一个函数 `func(session sqlx.Session) error {}` 中即可,如果事务中的操作返回任何错误, `Transact()` 都会自动回滚事务。
|
49
go-zero.dev/cn/naming-spec.md
Normal file
49
go-zero.dev/cn/naming-spec.md
Normal file
@ -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)
|
154
go-zero.dev/cn/online-exchange.md
Normal file
154
go-zero.dev/cn/online-exchange.md
Normal file
@ -0,0 +1,154 @@
|
||||
# 10月3日线上交流问题汇总
|
||||
|
||||
- go-zero适用场景
|
||||
- 希望说说应用场景,各个场景下的优势
|
||||
- 高并发的微服务系统
|
||||
- 支撑千万级日活,百万级QPS
|
||||
- 完整的微服务治理能力
|
||||
- 支持自定义中间件
|
||||
- 很好的管理了数据库和缓存
|
||||
- 有效隔离故障
|
||||
- 低并发的单体系统
|
||||
- 这种系统直接使用api层即可,无需rpc服务
|
||||
- 各个功能的使用场景以及使用案例
|
||||
- 限流
|
||||
- 熔断
|
||||
- 降载
|
||||
- 超时
|
||||
- 可观测性
|
||||
- go-zero的实际体验
|
||||
- 服务很稳
|
||||
- 前后端接口一致性,一个api文件即可生成前后端代码
|
||||
- 规范、代码量少,意味着bug少
|
||||
- 免除api文档,极大降低沟通成本
|
||||
- 代码结构完全一致,便于维护和接手
|
||||
- 微服务的项目结构, monorepo的 CICD 处理
|
||||
|
||||
```
|
||||
bookstore
|
||||
├── api
|
||||
│ ├── etc
|
||||
│ └── internal
|
||||
│ ├── config
|
||||
│ ├── handler
|
||||
│ ├── logic
|
||||
│ ├── svc
|
||||
│ └── types
|
||||
└── rpc
|
||||
├── add
|
||||
│ ├── adder
|
||||
│ ├── etc
|
||||
│ ├── internal
|
||||
│ │ ├── config
|
||||
│ │ ├── logic
|
||||
│ │ ├── server
|
||||
│ │ └── svc
|
||||
│ └── pb
|
||||
├── check
|
||||
│ ├── checker
|
||||
│ ├── etc
|
||||
│ ├── internal
|
||||
│ │ ├── config
|
||||
│ │ ├── logic
|
||||
│ │ ├── server
|
||||
│ │ └── svc
|
||||
│ └── pb
|
||||
└── model
|
||||
```
|
||||
|
||||
mono repo的CI我们是通过gitlab做的,CD使用jenkins
|
||||
CI尽可能更严格的模式,比如-race,使用sonar等工具
|
||||
CD有开发、测试、预发、灰度和正式集群
|
||||
晚6点上灰度、无故障的话第二天10点自动同步到正式集群
|
||||
正式集群分为多个k8s集群,有效的防止单集群故障,直接摘除即可,集群升级更有好
|
||||
- 如何部署,如何监控?
|
||||
- 全量K8S,通过jenkins自动打包成docker镜像,按照时间打包tag,这样可以一眼看出哪一天的镜像
|
||||
- 上面已经讲了,预发->灰度->正式
|
||||
- Prometheus+自建dashboard服务
|
||||
- 基于日志检测服务和请求异常
|
||||
- 如果打算换go-zero框架重构业务,如何做好线上业务稳定安全用户无感切换?另外咨询下如何进行服务划分?
|
||||
- 逐步替换,从外到内,加个proxy来校对,校对一周后可以切换
|
||||
- 如有数据库重构,则需要做好新老同步
|
||||
- 服务划分按照业务来,遵循从粗到细的原则,避免一个api一个微服务
|
||||
- 数据拆分对于微服务来讲尤为重要,上层好拆,数据难拆,尽可能保证按照业务拆分数据
|
||||
- 服务发现
|
||||
- 服务发现 etcd 的 key 的设计
|
||||
- 服务key+时间戳,服务进程数存在时间戳冲突的概率极低,忽略
|
||||
- etcd服务发现与治理, 异常捕获与处理异常
|
||||
- 为啥k8s还使用etcd做服务发现,因为dns的刷新有延迟,导致滚动更新会有大量失败,而etcd可以做到完全无损更新
|
||||
- etcd集群直接部署在k8s集群内,因为多个正式集群,集群单点和注册避免混乱
|
||||
- 针对etcd异常或者leader切换,自动侦测并刷新,当etcd有异常不能恢复时,不会刷新服务列表,保障服务依然可用
|
||||
- 缓存的设计与使用案例
|
||||
- 分布式多redis集群,线上最大几十个集群为同一个服务提供缓存服务
|
||||
- 无缝扩缩容
|
||||
- 不存在没有过期时间的缓存,避免大量不常使用的数据占用资源,默认一周
|
||||
- 缓存穿透,没有的数据会短暂缓存一分钟,避免刷接口或大量不存在的数据请求带垮系统
|
||||
- 缓存击穿,一个进程只会刷新一次同一个数据,避免热点数据被大量同时加载
|
||||
- 缓存雪崩,对缓存过期时间自动做了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 框架从设计到实践出书吗?框架以后的发展规划是什么?
|
||||
- 暂无出书计划,做好框架是最重要的
|
||||
- 继续着眼于工程效率
|
||||
- 提升服务治理能力
|
||||
- 帮助业务开发尽可能快速落地
|
127
go-zero.dev/cn/periodlimit.md
Normal file
127
go-zero.dev/cn/periodlimit.md
Normal file
@ -0,0 +1,127 @@
|
||||
# periodlimit
|
||||
|
||||
不管是在单体服务中还是在微服务中,开发者为前端提供的API接口都是有访问上限的,当访问频率或者并发量超过其承受范围时候,我们就必须考虑限流来保证接口的可用性或者降级可用性。即接口也需要安装上保险丝,以防止非预期的请求对系统压力过大而引起的系统瘫痪。
|
||||
|
||||
|
||||
本文就来介绍一下 `periodlimit` 。
|
||||
## 使用
|
||||
```go
|
||||
const (
|
||||
seconds = 1
|
||||
total = 100
|
||||
quota = 5
|
||||
)
|
||||
// New limiter
|
||||
l := NewPeriodLimit(seconds, quota, redis.NewRedis(s.Addr(), redis.NodeType), "periodlimit")
|
||||
|
||||
// take source
|
||||
code, err := l.Take("first")
|
||||
if err != nil {
|
||||
logx.Error(err)
|
||||
return true
|
||||
}
|
||||
|
||||
// switch val => process request
|
||||
switch code {
|
||||
case limit.OverQuota:
|
||||
logx.Errorf("OverQuota key: %v", key)
|
||||
return false
|
||||
case limit.Allowed:
|
||||
logx.Infof("AllowedQuota key: %v", key)
|
||||
return true
|
||||
case limit.HitQuota:
|
||||
logx.Errorf("HitQuota key: %v", key)
|
||||
// todo: maybe we need to let users know they hit the quota
|
||||
return false
|
||||
default:
|
||||
logx.Errorf("DefaultQuota key: %v", key)
|
||||
// unknown response, we just let the sms go
|
||||
return true
|
||||
}
|
||||
```
|
||||
## periodlimit
|
||||
|
||||
|
||||
`go-zero` 采取 **滑动窗口** 计数的方式,计算一段时间内对同一个资源的访问次数,如果超过指定的 `limit` ,则拒绝访问。当然如果你是在一段时间内访问不同的资源,每一个资源访问量都不超过 `limit` ,此种情况是允许大量请求进来的。
|
||||
|
||||
|
||||
而在一个分布式系统中,存在多个微服务提供服务。所以当瞬间的流量同时访问同一个资源,如何让计数器在分布式系统中正常计数? 同时在计算资源访问时,可能会涉及多个计算,如何保证计算的原子性?
|
||||
|
||||
|
||||
- `go-zero` 借助 `redis` 的 `incrby` 做资源访问计数
|
||||
- 采用 `lua script` 做整个窗口计算,保证计算的原子性
|
||||
|
||||
|
||||
|
||||
下面来看看 `lua script` 控制的几个关键属性:
|
||||
|
||||
| **argument** | **mean** |
|
||||
| --- | --- |
|
||||
| key[1] | 访问资源的标示 |
|
||||
| ARGV[1] | limit => 请求总数,超过则限速。可设置为 QPS |
|
||||
| ARGV[2] | window大小 => 滑动窗口,用 ttl 模拟出滑动的效果 |
|
||||
|
||||
```lua
|
||||
-- to be compatible with aliyun redis,
|
||||
-- we cannot use `local key = KEYS[1]` to reuse thekey
|
||||
local limit = tonumber(ARGV[1])
|
||||
local window = tonumber(ARGV[2])
|
||||
-- incrbt key 1 => key visis++
|
||||
local current = redis.call("INCRBY", KEYS[1], 1)
|
||||
-- 如果是第一次访问,设置过期时间 => TTL = window size
|
||||
-- 因为是只限制一段时间的访问次数
|
||||
if current == 1 then
|
||||
redis.call("expire", KEYS[1], window)
|
||||
return 1
|
||||
elseif current < limit then
|
||||
return 1
|
||||
elseif current == limit then
|
||||
return 2
|
||||
else
|
||||
return 0
|
||||
end
|
||||
```
|
||||
至于上述的 `return code` ,返回给调用方。由调用方来决定请求后续的操作:
|
||||
|
||||
| **return code** | **tag** | call code | **mean** |
|
||||
| --- | --- | --- | --- |
|
||||
| 0 | OverQuota | 3 | **over limit** |
|
||||
| 1 | Allowed | 1 | **in limit** |
|
||||
| 2 | HitQuota | 2 | **hit limit** |
|
||||
|
||||
下面这张图描述了请求进入的过程,以及请求触发 `limit` 时后续发生的情况:
|
||||
![image.png](https://cdn.nlark.com/yuque/0/2020/png/261626/1605430483430-92415ed3-e88f-487d-8fd6-8c58a9abe334.png#align=left&display=inline&height=524&margin=%5Bobject%20Object%5D&name=image.png&originHeight=524&originWidth=1051&size=90836&status=done&style=none&width=1051)
|
||||
![image.png](https://cdn.nlark.com/yuque/0/2020/png/261626/1605495120249-f6b05ac2-7090-47b0-a3c0-da50df6206dd.png#align=left&display=inline&height=557&margin=%5Bobject%20Object%5D&name=image.png&originHeight=557&originWidth=456&size=53785&status=done&style=none&width=456)
|
||||
## 后续处理
|
||||
|
||||
|
||||
如果在服务某个时间点,请求大批量打进来,`periodlimit` 短期时间内达到 `limit` 阈值,而且设置的时间范围还远远没有到达。后续请求的处理就成为问题。
|
||||
|
||||
|
||||
`periodlimit` 中并没有处理,而是返回 `code` 。把后续请求的处理交给了开发者自己处理。
|
||||
|
||||
|
||||
1. 如果不做处理,那就是简单的将请求拒绝
|
||||
1. 如果需要处理这些请求,开发者可以借助 `mq` 将请求缓冲,减缓请求的压力
|
||||
1. 采用 `tokenlimit`,允许暂时的流量冲击
|
||||
|
||||
|
||||
|
||||
所以下一篇我们就来聊聊 `tokenlimit`
|
||||
|
||||
|
||||
## 总结
|
||||
`go-zero` 中的 `periodlimit` 限流方案是基于 `redis` 计数器,通过调用 `redis lua script` ,保证计数过程的原子性,同时保证在分布式的情况下计数是正常的。但是这种方案存在缺点,因为它要记录时间窗口内的所有行为记录,如果这个量特别大的时候,内存消耗会变得非常严重。
|
||||
|
||||
|
||||
## 参考
|
||||
|
||||
|
||||
- [go-zero periodlimit](https://github.com/tal-tech/go-zero/blob/master/core/limit/periodlimit.go)
|
||||
- [分布式服务限流实战,已经为你排好坑了](https://www.infoq.cn/article/Qg2tX8fyw5Vt-f3HH673)
|
||||
- [tokenlimit](tokenlimit.md)
|
||||
|
||||
|
||||
|
||||
|
||||
|
16
go-zero.dev/cn/plugin-center.md
Normal file
16
go-zero.dev/cn/plugin-center.md
Normal file
@ -0,0 +1,16 @@
|
||||
# 插件中心
|
||||
goctl api提供了对plugin命令来支持对api进行功能扩展,当goctl api中的功能不满足你的使用,
|
||||
或者需要对goctl api进行功能自定义的扩展,那么插件功能将非常适合开发人员进行自给自足,详情见
|
||||
[goctl plugin](goctl-plugin.md)
|
||||
|
||||
## 插件资源
|
||||
* [goctl-go-compact](https://github.com/zeromicro/goctl-go-compact)
|
||||
goctl默认的一个路由一个文件合并成一个文件
|
||||
* [goctl-swagger](https://github.com/zeromicro/goctl-swagger)
|
||||
通过api文件生成swagger文档
|
||||
* [goctl-php](https://github.com/zeromicro/goctl-php)
|
||||
goctl-php是一款基于goctl的插件,用于生成 php 调用端(服务端) http server请求代码
|
||||
|
||||
# 猜你想看
|
||||
* [goctl插件](goctl-plugin.md)
|
||||
* [api语法介绍](api-grammar.md)
|
9
go-zero.dev/cn/prepare-other.md
Normal file
9
go-zero.dev/cn/prepare-other.md
Normal file
@ -0,0 +1,9 @@
|
||||
# 其他
|
||||
在之前我们已经对Go环境、Go Module配置、Goctl、protoc&protoc-gen-go安装准备就绪,这些是开发人员在开发阶段必须要准备的环境,而接下来的环境你可以选择性的安装,
|
||||
因为这些环境一般存在于服务器(安装工作运维会替你完成),但是为了后续**演示**流程能够完整走下去,我建议大家在本地也安装一下,因为我们的演示环境大部分会以本地为主。
|
||||
以下仅给出了需要的准备工作,不以文档篇幅作详细介绍了。
|
||||
|
||||
## 其他环境
|
||||
* [etcd](https://etcd.io/docs/current/rfc/v3api/)
|
||||
* [redis](https://redis.io/)
|
||||
* [mysql](https://www.mysql.com/)
|
8
go-zero.dev/cn/prepare.md
Normal file
8
go-zero.dev/cn/prepare.md
Normal file
@ -0,0 +1,8 @@
|
||||
# 准备工作
|
||||
在正式进入实际开发之前,我们需要做一些准备工作,比如:Go环境的安装,grpc代码生成使用的工具安装,
|
||||
必备工具Goctl的安装,Golang环境配置等,本节将包含以下小节:
|
||||
* [golang安装](golang-install.md)
|
||||
* [go modudle配置](gomod-config.md)
|
||||
* [goctl安装](goctl-install.md)
|
||||
* [protoc&protoc-gen-go安装](protoc-install.md)
|
||||
* [其他](prepare-other.md)
|
32
go-zero.dev/cn/project-dev.md
Normal file
32
go-zero.dev/cn/project-dev.md
Normal file
@ -0,0 +1,32 @@
|
||||
# 项目开发
|
||||
在前面的章节我们已经从一些概念、背景、快速入门等维度介绍了一下go-zero,看到这里,相信你对go-zero已经有了一些了解,
|
||||
从这里开始,我们将会从环境准备到服务部署整个流程开始进行讲解,为了保证大家能够彻底弄懂go-zero的开发流程,那就准备你的耐心来接着往下走吧。
|
||||
在章节中,将包含以下小节:
|
||||
* [准备工作](prepare.md)
|
||||
* [golang安装](golang-install.md)
|
||||
* [go modudle配置](gomod-config.md)
|
||||
* [goctl安装](goctl-install.md)
|
||||
* [protoc&protoc-gen-go安装](protoc-install.md)
|
||||
* [其他](prepare-other.md)
|
||||
* [开发规范](dev-specification.md)
|
||||
* [命名规范](naming-spec.md)
|
||||
* [路由规范](route-naming-spec.md)
|
||||
* [编码规范](coding-spec.md)
|
||||
* [开发流程](dev-flow.md)
|
||||
* [配置介绍](config-introduction.md)
|
||||
* [api配置](api-config.md)
|
||||
* [rpc配置](rpc-config.md)
|
||||
* [业务开发](business-dev.md)
|
||||
* [目录拆分](service-design.md)
|
||||
* [model生成](model-gen.md)
|
||||
* [api文件编写](api-coding.md)
|
||||
* [业务编码](business-coding.md)
|
||||
* [jwt鉴权](jwt.md)
|
||||
* [中间件使用](middleware.md)
|
||||
* [rpc服务编写与调用](rpc-call.md)
|
||||
* [错误处理](error-handle.md)
|
||||
* [CI/CD](ci-cd.md)
|
||||
* [服务部署](service-deployment.md)
|
||||
* [日志收集](log-collection.md)
|
||||
* [链路追踪](trace.md)
|
||||
* [服务监控](service-monitor.md)
|
56
go-zero.dev/cn/protoc-install.md
Normal file
56
go-zero.dev/cn/protoc-install.md
Normal file
@ -0,0 +1,56 @@
|
||||
# protoc & protoc-gen-go安装
|
||||
|
||||
## 前言
|
||||
protoc是一款用C++编写的工具,其可以将proto文件翻译为指定语言的代码。在go-zero的微服务中,我们采用grpc进行服务间的通信,而grpc的编写就需要用到protoc和翻译成go语言rpc stub代码的插件protoc-gen-go。
|
||||
|
||||
本文演示环境
|
||||
* mac OS
|
||||
* protoc 3.14.0
|
||||
|
||||
## protoc安装
|
||||
|
||||
* 进入[protobuf release](https://github.com/protocolbuffers/protobuf/releases) 页面,选择适合自己操作系统的压缩包文件
|
||||
* 解压`protoc-3.14.0-osx-x86_64.zip`并进入`protoc-3.14.0-osx-x86_64`
|
||||
```shell
|
||||
$ cd protoc-3.14.0-osx-x86_64/bin
|
||||
```
|
||||
* 将启动的`protoc`二进制文件移动到被添加到环境变量的任意path下,如`$GOPATH/bin`,这里不建议直接将其和系统的以下path放在一起。
|
||||
```shell
|
||||
$ mv protoc $GOPATH/bin
|
||||
```
|
||||
> [!TIP]
|
||||
> $GOPATH为你本机的实际文件夹地址
|
||||
* 验证安装结果
|
||||
```shell
|
||||
$ protoc --version
|
||||
```
|
||||
```shell
|
||||
libprotoc 3.14.0
|
||||
```
|
||||
|
||||
## protoc-gen-*安装
|
||||
在goctl版本大于1.2.1时,则不需要安装 `protoc-gen-go` 插件了,因为在该版本以后,goctl已经实现了作为 `protoc` 的插件了,goctl在指定 `goctl xxx` 命令时会自动
|
||||
将 `goctl` 创建一个符号链接 `protoc-gen-goctl` ,在生成pb.go时会按照如下逻辑生成:
|
||||
1. 检测环境变量中是否存在 `protoc-gen-goctl` 插件,如果是,则跳转到第3步
|
||||
2. 检测环境变量中是否存在 `protoc-gen-go` 插件,如果不存在,则生成流程结束
|
||||
3. 根据检测到的插件生成pb.go
|
||||
|
||||
> [!TIPS]
|
||||
>
|
||||
> Windows 在创建符号链接可能会报错, `A required privilege is not held by the client.`, 原因是在Windows下执行goctl需要"以管理员身份"运行。
|
||||
>
|
||||
|
||||
* 下载安装`protoc-gen-go`
|
||||
|
||||
如果goctl 版本已经是1.2.1以后了,可以忽略此步骤。
|
||||
```shell
|
||||
$ go get -u github.com/golang/protobuf/protoc-gen-go@v1.3.2
|
||||
```
|
||||
```text
|
||||
go: found github.com/golang/protobuf/protoc-gen-go in github.com/golang/protobuf v1.4.3
|
||||
go: google.golang.org/protobuf upgrade => v1.25.0
|
||||
```
|
||||
* 将protoc-gen-go移动到被添加环境变量的任意path下,如`$GOPATH/bin`,由于`go get`后的二进制本身就在`$GOPATH/bin`目录中,因此只要确保你的`$GOPATH/bin`在环境变量即可。
|
||||
|
||||
> **[!WARNING]
|
||||
> protoc-gen-go安装失败请阅读[常见错误处理](error.md)
|
6
go-zero.dev/cn/quick-start.md
Normal file
6
go-zero.dev/cn/quick-start.md
Normal file
@ -0,0 +1,6 @@
|
||||
# 快速开发
|
||||
|
||||
本节主要通过对 api/rpc 等服务快速开始来让大家对使用 go-zero 开发的工程有一个宏观概念,更加详细的介绍我们将在后续一一展开。如果您已经参考 [准备工作](prepare.md) 做好环境及工具的准备,请跟随以下小节开始体验:
|
||||
|
||||
* [单体服务](monolithic-service.md)
|
||||
* [微服务](micro-service.md)
|
269
go-zero.dev/cn/redis-cache.md
Normal file
269
go-zero.dev/cn/redis-cache.md
Normal file
@ -0,0 +1,269 @@
|
||||
# go-zero缓存设计之持久层缓存
|
||||
|
||||
## 缓存设计原理
|
||||
|
||||
我们对缓存是只删除,不做更新,一旦DB里数据出现修改,我们就会直接删除对应的缓存,而不是去更新。
|
||||
|
||||
我们看看删除缓存的顺序怎样才是正确的。
|
||||
|
||||
* 先删除缓存,再更新DB
|
||||
|
||||
![redis-cache-01](./resource/redis-cache-01.png)
|
||||
|
||||
我们看两个并发请求的情况,A请求需要更新数据,先删除了缓存,然后B请求来读取数据,此时缓存没有数据,就会从DB加载数据并写回缓存,然后A更新了DB,那么此时缓存内的数据就会一直是脏数据,直到缓存过期或者有新的更新数据的请求。如图
|
||||
|
||||
![redis-cache-02](./resource/redis-cache-02.png)
|
||||
|
||||
* 先更新DB,再删除缓存
|
||||
|
||||
![redis-cache-03](./resource/redis-cache-03.png)
|
||||
|
||||
A请求先更新DB,然后B请求来读取数据,此时返回的是老数据,此时可以认为是A请求还没更新完,最终一致性,可以接受,然后A删除了缓存,后续请求都会拿到最新数据,如图
|
||||
![redis-cache-04](./resource/redis-cache-04.png)
|
||||
|
||||
让我们再来看一下正常的请求流程:
|
||||
|
||||
* 第一个请求更新DB,并删除了缓存
|
||||
* 第二个请求读取缓存,没有数据,就从DB读取数据,并回写到缓存里
|
||||
* 后续读请求都可以直接从缓存读取
|
||||
![redis-cache-05](./resource/redis-cache-05.png)
|
||||
|
||||
我们再看一下DB查询有哪些情况,假设行记录里有ABCDEFG七列数据:
|
||||
|
||||
* 只查询部分列数据的请求,比如请求其中的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)
|
142
go-zero.dev/cn/redis-lock.md
Normal file
142
go-zero.dev/cn/redis-lock.md
Normal file
@ -0,0 +1,142 @@
|
||||
# redis-lock
|
||||
|
||||
# redis lock
|
||||
|
||||
既然是锁,首先想到的一个作用就是:**防重复点击,在一个时间点只有一个请求产生效果**。
|
||||
|
||||
|
||||
而既然是 `redis`,就得具有排他性,同时也具有锁的一些共性:
|
||||
|
||||
|
||||
- 高性能
|
||||
- 不能出现死锁
|
||||
- 不能出现节点down掉后加锁失败
|
||||
|
||||
|
||||
|
||||
`go-zero` 中利用 redis `set key nx` 可以保证key不存在时写入成功,`px` 可以让key超时后自动删除「最坏情况也就是超时自动删除key,从而也不会出现死锁」
|
||||
|
||||
|
||||
## example
|
||||
|
||||
|
||||
```go
|
||||
redisLockKey := fmt.Sprintf("%v%v", redisTpl, headId)
|
||||
// 1. New redislock
|
||||
redisLock := redis.NewRedisLock(redisConn, redisLockKey)
|
||||
// 2. 可选操作,设置 redislock 过期时间
|
||||
redisLock.SetExpire(redisLockExpireSeconds)
|
||||
if ok, err := redisLock.Acquire(); !ok || err != nil {
|
||||
return nil, errors.New("当前有其他用户正在进行操作,请稍后重试")
|
||||
}
|
||||
defer func() {
|
||||
recover()
|
||||
// 3. 释放锁
|
||||
redisLock.Release()
|
||||
}()
|
||||
```
|
||||
|
||||
|
||||
和你在使用 `sync.Mutex` 的方式时一致的。加锁解锁,执行你的业务操作。
|
||||
|
||||
|
||||
## 获取锁
|
||||
|
||||
|
||||
```go
|
||||
lockCommand = `if redis.call("GET", KEYS[1]) == ARGV[1] then
|
||||
redis.call("SET", KEYS[1], ARGV[1], "PX", ARGV[2])
|
||||
return "OK"
|
||||
else
|
||||
return redis.call("SET", KEYS[1], ARGV[1], "NX", "PX", ARGV[2])
|
||||
end`
|
||||
|
||||
func (rl *RedisLock) Acquire() (bool, error) {
|
||||
seconds := atomic.LoadUint32(&rl.seconds)
|
||||
// execute luascript
|
||||
resp, err := rl.store.Eval(lockCommand, []string{rl.key}, []string{
|
||||
rl.id, strconv.Itoa(int(seconds)*millisPerSecond + tolerance)})
|
||||
if err == red.Nil {
|
||||
return false, nil
|
||||
} else if err != nil {
|
||||
logx.Errorf("Error on acquiring lock for %s, %s", rl.key, err.Error())
|
||||
return false, err
|
||||
} else if resp == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
reply, ok := resp.(string)
|
||||
if ok && reply == "OK" {
|
||||
return true, nil
|
||||
} else {
|
||||
logx.Errorf("Unknown reply when acquiring lock for %s: %v", rl.key, resp)
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
先介绍几个 `redis` 的命令选项,以下是为 `set` 命令增加的选项:
|
||||
|
||||
|
||||
- `ex seconds` :设置key过期时间,单位s
|
||||
- `px milliseconds` :设置key过期时间,单位毫秒
|
||||
- `nx`:key不存在时,设置key的值
|
||||
- `xx`:key存在时,才会去设置key的值
|
||||
|
||||
|
||||
|
||||
其中 `lua script` 涉及的入参:
|
||||
|
||||
|
||||
|
||||
| args | 示例 | 含义 |
|
||||
| --- | --- | --- |
|
||||
| KEYS[1] | key$20201026 | redis key |
|
||||
| ARGV[1] | lmnopqrstuvwxyzABCD | 唯一标识:随机字符串 |
|
||||
| ARGV[2] | 30000 | 设置锁的过期时间 |
|
||||
|
||||
|
||||
|
||||
然后来说说代码特性:
|
||||
|
||||
|
||||
1. `Lua` 脚本保证原子性「当然,把多个操作在 Redis 中实现成一个操作,也就是单命令操作」
|
||||
1. 使用了 `set key value px milliseconds nx`
|
||||
1. `value` 具有唯一性
|
||||
1. 加锁时首先判断 `key` 的 `value` 是否和之前设置的一致,一致则修改过期时间
|
||||
|
||||
|
||||
|
||||
## 释放锁
|
||||
|
||||
|
||||
```go
|
||||
delCommand = `if redis.call("GET", KEYS[1]) == ARGV[1] then
|
||||
return redis.call("DEL", KEYS[1])
|
||||
else
|
||||
return 0
|
||||
end`
|
||||
|
||||
func (rl *RedisLock) Release() (bool, error) {
|
||||
resp, err := rl.store.Eval(delCommand, []string{rl.key}, []string{rl.id})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if reply, ok := resp.(int64); !ok {
|
||||
return false, nil
|
||||
} else {
|
||||
return reply == 1, nil
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
释放锁的时候只需要关注一点:
|
||||
|
||||
|
||||
**不能释放别人的锁,不能释放别人的锁,不能释放别人的锁**
|
||||
|
||||
|
||||
所以需要先 `get(key) == value「key」`,为 true 才会去 `delete`
|
BIN
go-zero.dev/cn/resource/3aefec98-56eb-45a6-a4b2-9adbdf4d63c0.png
Normal file
BIN
go-zero.dev/cn/resource/3aefec98-56eb-45a6-a4b2-9adbdf4d63c0.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 45 KiB |
Binary file not shown.
After Width: | Height: | Size: 250 KiB |
Binary file not shown.
After Width: | Height: | Size: 229 KiB |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user