参考文档

开始

  • 安装wire,参见下面常用包章节
  • 使用goland,import google包报红,参考这个解决,settings->protocol buffers->location中加入third_party路径

项目结构相关

  • api目录下.proto文件可修改,经过protoc自动转为.pb.go文件
  • cmd目录下,wire-gen.go用来做依赖注入,自动生成的
  • biz目录下实现业务逻辑
  • openapi.yaml可以可视化展示api,借助SwaggerEditor

API定义与生成

api目录下的.proto文件中定义api

1
2
//编译生成api
make api

定义rpc

api定义中路径,:对应为{},例如

1
2
3
4
//  GET /api/profiles/:username
option (google.api.http) = {
get: "/api/profiles/{username}",
};

增删改查例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
rpc GetArticle(GetArticleRequest) returns (SingleArticleReply){
option (google.api.http) = {
get: "/api/articles/{slug}",
};
}

rpc CreateArticle(CreateArticleRequest) returns (SingleArticleReply){
option (google.api.http) = {
post: "/api/articles",
body: "*",
};
}

rpc UpdateArticle(UpdateArticleRequest) returns (SingleArticleReply){
option (google.api.http) = {
put: "/api/articles/{slug}",
body: "*",
};
}

rpc DeleteArticle(DeleteArticleRequest) returns (google.protobuf.Empty){
option (google.api.http) = {
delete: "DELETE /api/articles/{slug}",
};
}

message例子

1
2
3
4
5
6
7
8
9
10
11
message CreateArticleRequest {

message Article {
string title = 1;
string description = 2;
string body = 3;
repeated string tagList = 4;
}

Article article = 1;
}

空message

1
2
3
import "google/protobuf/empty.proto";

rpc DeleteArticle(DeleteArticleRequest) returns (google.protobuf.Empty)

生成 Service 代码

CLI工具,通过 proto文件,可以直接生成对应的 Service 实现代码模板

1
kratos proto server api/realworld/v1/realworld.proto -t internal/service

连接数据库

  • 安装docker

  • 写好docker的yaml文件

    1
    2
    3
    4
    5
    6
    7
    8
    version: '3'
    services:
    rwdb:
    image: mysql:8
    environment:
    MYSQL_ROOT_PASSWORD: dangerous
    ports:
    - "3306:3306"
  • 启动docker docker-compose up -d

  • 登录数据库,创建database

  • 安装GORM,参考文档,连接mysql例子

密码哈希加密解密

使用bcrypt包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func hashPassword(pwd string) string {
b, err := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost)
if err != nil {
panic(err)
}
fmt.Printf("%v", b)
return string(b)
}

func verifyPassword(hashed, input string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hashed), []byte(input))
if err != nil {
return false
}
return true
}

CORS和自定义http中间件

跨域资源共享, 用到的库是gorilla/handlers

跨域资源共享 CORS 详解 - 阮一峰的网络日志

自定义http中间件,实现对一些甚至全部的Http Request统一处理。使用http.Filter()

1
2
3
4
5
6
7
8
//http.go
http.Filter(
handlers.CORS(
handlers.AllowedHeaders([]string{"X-Requested-With", "Content-Type", "Authorization"}),
handlers.AllowedMethods([]string{"GET", "POST", "PUT", "HEAD", "OPTIONS"}),
handlers.AllowedOrigins([]string{"*"}),
),
),

自定义Error类型

目标:将业务抛出的error序列化后写入Response Body中,并设置HTTP Status Code

  • 自定义Error类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    // 目录errors/errors.go
    package errors

    import (
    "errors"
    "fmt"
    )

    //创建一个新的Error类型,实现Error()方法,即可定制自己的Error
    type HTTPError struct {
    Errors map[string][]string `json:"errors"`
    Code int `json:"-"`
    }

    func (e *HTTPError) Error() string {
    return fmt.Sprintf("HTTPError: %d", e.Code)
    }

    //业务层使用时调用这个函数,创建Error
    func NewHTTPError(code int, field string, detail string) *HTTPError {
    return &HTTPError{
    Code: code,
    Errors: map[string][]string{
    field: {detail},
    },
    }
    }

    // error实体转换为*HTTPError实体
    func FromError(err error) *HTTPError {
    if err == nil {
    return nil
    }
    if se := new(HTTPError); errors.As(err, &se) {
    return se
    }
    return &HTTPError{}
    }
  • Error序列化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    // 目录server/encoder.go
    package server

    import (
    "github.com/go-kratos/kratos/v2/transport/http"
    "kratos-realworld/internal/errors"
    nethttp "net/http"
    )

    func errorEncoder(w nethttp.ResponseWriter, r *nethttp.Request, err error) {
    se := errors.FromError(err)
    codec, _ := http.CodecForRequest(r, "Accept")
    body, err := codec.Marshal((se))
    if err != nil {
    w.WriteHeader(500)
    return
    }
    w.Header().Set("Content-Type", "application/"+codec.Name())

    if se.Code > 99 && se.Code < 600 {
    w.WriteHeader(se.Code)
    } else {
    w.WriteHeader(500)
    }

    _, _ = w.Write(body)
    }
1
2
3
4
5
6
7
8
9
10
- http服务中注册

```go
// 目录server/http.go
func NewHTTPServer(c *conf.Server, jwtc *conf.JWT, greeter *service.RealWorldService, logger log.Logger) *http.Server {
var opts = []http.ServerOption{
http.ErrorEncoder(errorEncoder),
...
}
}
  • 业务层使用
    1
    2
    3
    4
    5
    6
    7
    func (s *RealWorldService) Login(ctx context.Context, req *pb.LoginRequest) (*pb.UserReply, error) {
    if len(req.User.Email) == 0 {
    return nil, errors.NewHTTPError(422, "email", "cannot be empty")
    }

    ...
    }

biz层

biz层中完成了以下任务:

  • 定义了biz层的实体类型Xxx,比如type User struct

  • 定义了接口XxxRepo,里面定义了data层的函数,具体实现在data层写 ;

  • 定义了结构体XxxUseCase,结构体内是XxxRepo,biz层的方法都绑定在该结构体上;

  • NewXxxUsecase()函数,依赖注入

  • 实现业务逻辑,通过调用data层的函数来操作数据库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 目录:biz/user.go

//定义biz层的实体类型
type User struct {
Email string
Username string
...
}

//定义接口和data层函数
type UserRepo interface {
CreateUser(ctx context.Context, user *User) error
...
}

//定义结构体XxxUseCase,结构体内是XxxRepo
type UserUsecase struct {
ur UserRepo
...
log *log.Helper
}

//依赖注入
func NewUserUsecase(ur UserRepo, pr ProfileRepo, jwtc *conf.JWT, logger log.Logger) *UserUsecase {
return &UserUsecase{ur: ur, pr: pr, jwtc: jwtc, log: log.NewHelper(logger)}
}

//实现业务逻辑,创建biz层实体,调用data层函数
func (uc *UserUsecase) Register(ctx context.Context, username, email, password string) (*UserLogin, error) {
u := &User{
Email: email,
Username: username,
PasswordHash: hashPassword(password),
}

if err := uc.ur.CreateUser(ctx, u); err != nil {
return nil, err
}

return &UserLogin{
Email: email,
Username: username,
Token: uc.GenerateToken(username),
}, nil
}
1
2
3
4
5
6
7
// 目录:biz/biz.go
package biz

import "github.com/google/wire"

// ProviderSet is biz providers.
var ProviderSet = wire.NewSet(NewUserUsecase, NewSocialUsecase)

data层

data层完成了以下任务:

  • 连接启动数据库(参考连接数据库章节)

  • 定义了结构体xxxRepo,结构体内是操作数据库的类型*Data,data层的方法都绑定在该结构体上;

  • 定义了data层的实体类型Xxx,比如type User struct,字段对应了数据库表中的列名;

  • NewXxxRepo()函数,依赖注入

  • 实现biz层中接口定义的函数,操作数据库CRUD

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 目录:data/user.go

//定义结构体xxxRepo
type userRepo struct {
data *Data
log *log.Helper
}

//定义data层的实体类型,与数据库表对应
type User struct {
gorm.Model
Email string `gorm:"size:500"`
Username string `gorm:"size:500"`
...
}

//依赖注入
func NewUserRepo(data *Data, logger log.Logger) biz.UserRepo {
return &userRepo{
data: data,
log: log.NewHelper(logger),
}
}

//实现CRUD方法,与biz接口中的对应
func (r *userRepo) CreateUser(ctx context.Context, u *biz.User) error {
user := User{
Email: u.Email,
Username: u.Username,
Bio: u.Bio,
Image: u.Image,
PasswordHash: u.PasswordHash,
}
rv := r.data.db.Create(&user)
return rv.Error
}
1
2
3
4
5
6
7
8
9
// 目录:data/data.go

//操作数据库的结构体
type Data struct {
db *gorm.DB
}

//依赖注入
var ProviderSet = wire.NewSet(NewData, NewDB, NewUserRepo, NewProfileRepo)

测试

testing

函数传入参数testing.T 用来测试

assert

1
assert.NoError()//

常用包和工具

json转protobuf

在线转换

wire

依赖注入包

1
go install github.com/google/wire/cmd/wire@latest`

在wire.go所在目录运行 wire 即可

通过New() 函数来给结构体赋值,使用var ProviderSet = wire.NewSet(NewFuncName) 即可将该New() 函数加入自动依赖注入。

SwaggerEditor

方便与前端沟通接口
https://editor.swagger.io/

spew

spew.Dump(varName) 打印变量varName,支持打印各种类型,包括struct,map等,方便调试。

bcrypt

哈希加密包,可以用来给密码哈希加密和解密

文档

json

Go的json解析:Marshal与Unmarshal

  • Marshal:将数据编码成json字符串

    func Marshal(v interface{}) ([]byte, error)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    type Stu struct {
    Name string `json:"name"`
    Age int
    HIgh bool
    sex string
    Class *Class `json:"class"`
    }

    type Class struct {
    Name string
    Grade int
    }

    func main() {
    //实例化一个数据结构,用于生成json字符串
    stu := Stu{
    Name: "张三",
    Age: 18,
    HIgh: true,
    sex: "男",
    }

    //指针变量
    cla := new(Class)
    cla.Name = "1班"
    cla.Grade = 3
    stu.Class=cla

    //Marshal失败时err!=nil
    jsonStu, err := json.Marshal(stu)
    if err != nil {
    fmt.Println("生成json字符串错误")
    }

    //jsonStu是[]byte类型,转化成string类型便于查看
    fmt.Println(string(jsonStu))
    }

    //结果
    //
  • Unmarshal:将json字符串解码到相应的数据结构

    func Unmarshal(data []byte, v interface{}) error

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    type StuRead struct {
    Name interface{} `json:"name"`
    Age interface{}
    HIgh interface{}
    sex interface{}
    Class interface{} `json:"class"`
    Test interface{}
    }

    type Class struct {
    Name string
    Grade int
    }

    func main() {
    //json字符中的"引号,需用\进行转义,否则编译出错
    //json字符串沿用上面的结果,但对key进行了大小的修改,并添加了sex数据
    data:="{\"name\":\"张三\",\"Age\":18,\"high\":true,\"sex\":\"男\",\"CLASS\":{\"naME\":\"1班\",\"GradE\":3}}"
    str:=[]byte(data)

    //1.Unmarshal的第一个参数是json字符串,第二个参数是接受json解析的数据结构。
    //第二个参数必须是指针,否则无法接收解析的数据,如stu仍为空对象StuRead{}
    //2.可以直接stu:=new(StuRead),此时的stu自身就是指针
    stu:=StuRead{}
    err:=json.Unmarshal(str,&stu)

    //解析失败会报错,如json字符串格式不对,缺"号,缺}等。
    if err!=nil{
    fmt.Println(err)
    }

    fmt.Println(stu)
    }

    //结果
    //{张三 18 true <nil> map[naME:1班 GradE:3] <nil>}