这是我参与「第五届青训营」伴学笔记创作活动的第 6 天
一、本堂课重点内容
本节课程主要讲解Go框架三件套:
- ORM框架:Gorm
- RPC框架:Kitex
- HTTP框架:Hertz
二、详细知识点介绍
1 基本概念
- ORM(Object Relational Mapping,对象关系映射):主要作用是在编程中,把面向对象的概念跟数据库中表的概念对应起来。
- DSN(Data Source Name,数据源名称):应用程序用来请求与ODBC数据源连接的名称。连接到ODBC时,它会存储连接详细信息,例如数据库名称、目录、数据库驱动程序、UserID、password等。
- RPC(Remote Procedure Call,远程过程调用):RPC是指可以调用远端机器的函数或方法,而不需要关心底层的网络细节的思想。对于调用者来说,和调用本地方法没有什么区别。
- IDL(Interface definition language,接口描述语言):是一种语言的通用术语,它允许用一种语言编写的程序或对象与用未知语言编写的另一个程序进行通信,我们可以使用 IDL 来支持 RPC 的信息传输定义。
2 Gorm的基本使用
-
官方文档:gorm.io/zh_CN/docs/…
-
样例
-
约定
- 使用结构体中名为
ID的字段作为主键。 - 如果没有定义
TableName方法,则使用结构体的蛇形负数作为表名。 - 字段名的蛇形作为列名。
- 使用
CreatedAt、UpdatedAt字段作为创建、更新时间。
- 使用结构体中名为
-
代码细节
-
创建数据细节
- 如何进行upsert:使用
clause.OnConflict。 - 如何使用默认值:在结构体中使用
default标签。
- 如何进行upsert:使用
-
查询数据细节
-
使用
First时,需要注意查询不到数据会返回ErrRecordNotFound。 -
一般使用
.Where().Find()查询多条数据,查询不到数据不会返回错误。 -
当使用结构体作为条件查询时,只会查询非零值字段,如果需要使用零值,需要用Map来构建查询条件或使用Select选择字段。
db.Where(map[string]interface{}{"Name": "jinzhu", "Age": 0}).Find(&users)
-
-
更新数据细节
- 使用Struct更新时,只会更新非零值,如果需要使用零值,需要用Map来构建查询条件或使用Select选择字段。
-
删除数据细节
- 物理删除:
db.Delete() - 软删除:在结构体中定义
gorm.DeleteAt字段来实现软删,调用Delete时记录不会被从数据库中真正删除,使用Unscoped可以查询到被软删的数据。
- 物理删除:
-
-
事务
-
方法:
Begin、Commit、Rollback。 -
样例
-
推荐:
Tansaction方法用于自动提交事务,避免用户漏写Commit、Rollback。db.Transaction(func(tx *gorm.DB) error { // 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db') if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil { // 返回任何错误都会回滚事务 return err } if err := tx.Create(&Animal{Name: "Lion"}).Error; err != nil { return err } // 返回 nil 提交事务 return nil })
-
-
Hook
- Hook是在创建、查询、更新、删除等操作之前、之后自动调用的函数。
- 如果任何Hook返回错误,GORM将停止后续的操作并回滚事务。
-
性能提高
- 使用
SkipDefaultTransaction关闭默认事务。 - 使用
PrepareStmt缓存预编译语句,提高后续调用的速度。
- 使用
3 Kitex的基本使用
-
Kitex是字节跳动内部的 Golang 微服务 RPC 框架,具有高性能、强可扩展的特点。Kitex 目前对 Windows 的支持并不完善,建议使用虚拟机或 WSL2 进行测试。
-
安装 Kitex 代码生成工具
go install github.com/cloudwego/kitex/tool/cmd/kitex@latestgo install github.com/cloudwego/thriftgo@latest- 在此之前,请务必检查已正确设置
GOPATH环境变量,并将$GOPATH/bin添加到PATH环境变量中。
-
定义IDL
-
如果我们要进行PRC,就需要知道对方的接口是什么,需要传什么参数,同时也需要知道返回值是什么样的,这个时候,就需要通过IDL来约定双方的协议。
-
Kitex 默认支持 thrift 和 proto3 两种 IDL,而在底层传输上,Kitex 使用扩展的 thrift 作为底层的传输协议。
- Thrift IDL 语法:link.juejin.cn/?target=htt…
- proto3 语法:link.juejin.cn/?target=htt…
-
样例(命名为echo.thrift)
namespace go api struct Request { 1: string message } struct Response { 1: string message } service Echo { Response echo(1: Request req) }
-
-
Kitex生成代码
-
命令:
kitex -module example -service example echo.thrift-module表示生成的该项目的 go module 名。-service表明我们要生成一个服务端项目,后面紧跟的example为该服务的名字。- 最后一个参数则为该服务的 IDL 文件。
- 服务默认监听8888端口。
-
目录结构
-
文件说明
-
build.sh:构建脚本
-
kitex_gen:IDL内容相关的生成代码,主要是基础的Server/Client代码
-
main.go:程序入口
-
handler.go:用户在该文件里实现IDLservice定义的方法
package main import ( "context" api "exmaple/kitex_gen/api" ) // EchoImpl implements the last service interface defined in the IDL. type EchoImpl struct{} // Echo implements the EchoImpl interface. func (s *EchoImpl) Echo(ctx context.Context, req *api.Request) (resp *api.Response, err error) { // TODO: Your code here... return }
-
-
-
Kitex Client发起请求
-
创建Client
c, err := echo.NewClient("example", client.WithHostPorts("0.0.0.0:8888")) if err != nil { log.Fatal(err) } -
发起请求
req := &api.Request{Message: "my request"} resp, err := c.Echo(context.Background(), req, callopt.WithRPCTimeout(3*time.Second)) if err != nil { log.Fatal(err) } log.Println(resp)
-
-
Kitex服务注册与发现
-
Kitex已对接了主流的服务注册与发现中心,如ETCD, Nacos等。
-
服务注册
r, err :=etcd.NewEtcdRegistry([]string{"127.0.0.1:2379"}) server := hello.NewServer(new(HelloImpl), server.WithRegistry(r), server.WithServerBasicInfo(&rpcinfo.EndpointBasicInfo{ServiceName:"Hello",})) err = server.Run() -
服务发现
r, err :=etcd.NewEtcdResolver([]string{"127.0.0.1:2379"}) client := hello.MustNewClient("Hello", client.WithResolver(r))
-
4 Hertz的基本使用
-
Hertz是一个 Golang 微服务 HTTP 框架,具有高易用性、高性能、高扩展性等特点。
-
使用 Hertz(服务端)
-
基本使用
h := server.Default(server.WithHostPoerts("127.0.0.1:8080")) // server.New和server.Default的区别在于Default默认会继承一个recover中间件,如果有自定义中间件格式的需求可以用New h.GET("/ping", func(c context.Context, ctx *app.RequestContext) { ctx.JSON(consts.StatusOK, utils.H{"ping": "pong"}) }) // 注意Hertz有两个上下文,一个专注于传递源信息,一个专注于请求的处理 h.Spin() // 开启自旋 -
注册路由的方法
h.GET("/get", func(ctx context.Context, c *app.RequestContext) { c.String(consts.StatusOK, "get") }) h.POST("/post", func(ctx context.Context, c *app.RequestContext) { c.String(consts.StatusOK, "post") }) h.PUT("/put", func(ctx context.Context, c *app.RequestContext) { c.String(consts.StatusOK, "put") }) h.DELETE("/delete", func(ctx context.Context, c *app.RequestContext) { c.String(consts.StatusOK, "delete") }) h.PATCH("/patch", func(ctx context.Context, c *app.RequestContext) { c.String(consts.StatusOK, "patch") }) h.HEAD("/head", func(ctx context.Context, c *app.RequestContext) { c.String(consts.StatusOK, "head") }) h.OPTIONS("/options", func(ctx context.Context, c *app.RequestContext) { c.String(consts.StatusOK, "options") }) h.Any("/ping_any", func(ctx context.Context, c *app.RequestContext) { c.String(consts.StatusOK, "any") }) h.Handle("LOAD","/load", func(ctx context.Context, c *app.RequestContext) { c.String(consts.StatusOK, "load") }) // 自定义method可以使用Handle方法 -
路由组
v1 := h.Group("/v1") { v1.POST("/login", loginEndpoint) v1.POST("/submit", submitEndpoint) ... } v2 := h.Group("/v2") { v2.POST("/login", loginEndpoint) v2.POST("/submit", submitEndpoint) ... } -
参数路由和统配路由:优先级为静态路由>命名路由>统配路由。
-
参数路由:使用
:name这样的命名参数设置路由,并且命名参数只匹配单个路径段。// This handler will match: "/hertz/version", but will not match : "/hertz/" or "/hertz" h.GET("/hertz/:version", func(ctx context.Context, c *app.RequestContext) { version := c.Param("version") c.String(consts.StatusOK, "Hello %s", version) }) -
通配路由:使用
*path这样的通配参数设置路由,并且通配参数会匹配所有内容。// This one will match "/hertz/v1/" and "/hertz/v2/send" h.GET("/hertz/:version/*action", func(ctx context.Context, c *app.RequestContext) { version := c.Param("version") action := c.Param("action") message := version + " is " + action c.String(consts.StatusOK, message) })
-
-
参数绑定和校验
h.GET("/hello", func(ctx context.Context, c *app.RequestContext) { type Args struct{ Vd int `query:"vd" vd:"$==0||$==1"` ... } var arg Args err := c.Bind(&arg) // validate方法需要和go tag配合使用 err = c.Validate(&arg) err = c.BindAndValidate(&arg) }) -
中间件(服务端)
- 终止中间件调用链的执行:
c.Abort,c.AbortWithMsg,c.AbortWithStats - 可以给路由组注册中间件,也可以使用全局中间件。
func MyMiddleware() app.HandlerFunc { return func(ctx context.Context, c *app.RequestContext) { // pre-handle // ... c.Next(ctx) // call the next middleware(handler) // post-handle // ... } } func main() { h := server.Default(server.WithHostPort("127.0.0.1:8080")) h.Use(MyMiddleware()) //给全局注册中间件 h.Get("/middleware",func(ctx context.Context, c *app.RequestContext) { c.String(consts.StatusOK, "Hello hertz!") }) h.Spin() } - 终止中间件调用链的执行:
-
-
使用 Hertz(客户端)
提供HTTP Client帮助用户发送HTTP请求:
c, err := client.NewClient() if err != nil { return } // send http get request status, body, _ := c.Get(context.Background(), nil, "https://www.example.com") fmt.Printf("status=%v body=%v\n", status, string(body)) // send http post request var postArgs protocol.Args postArgs.Set("arg","a") // Set post args status, body, _ = c.Post(context.Background(), nil, "https://www.example.com", &postArgs) fmt.Printf("status=%v body=%v\n", status, string(body)) -
代码生成工具Hz
-
通过定义IDL文件即可生成对应的基础服务端代码:
namespace go hello.exanple struct HelloReq{ 1: string Name (api.query="name"); } struct HelloResp{ 1: string RespBody; } service HelloService { HelloResp HelloMethod(1: HelloReq request) (api.get="/hello"); } -
目录结构
-