零基础之go语言9 | 青训营笔记

85 阅读6分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 9 天

本次笔记将记录go的框架三件套:GORM、Kitex和Hertz

一些概念

  • DSN:数据源名称,即Data Source Name,指当我们的数据库建立好之后,系统需要知道数据的来源,所以就需要设定系统的 DSN(数据来源名称)。

  • ORM:对象关系映射,即Object-Relationl Mapping,是在关系型数据库和对象之间作一个映射。

  • RPC:远程过程调用,即Remote Procedure Call。它是利用网络从远程计算机上请求服务,可以理解为把程序的一部分放在其他远程计算机上执行。

  • IDL:接口描述语言,即Interface description language,是跨平台开发的基础。对于多个不同的平台、主机一起开发某个项目时,IDL用于集成。

  • GORM:Go中一款性能极好的ORM库,数据表对应结构体,数据行对应结构体实例,数据库字段对应结构体字段。个人感觉与MyBatis差不多。

  • Kitex:字节内部的Golang微服务RPC框架,具有高性能、强可扩展性的特点。

  • Hertz:字节内部的HTTP框架,具有高易用性、高性能、高扩展性的特点。用于注册路由。

GORM

GORM官网指南:
https://gorm.io/zh_CN/docs/index.html

安装

go get -u gorm.io/gorm 
go get -u gorm.io/driver/sqlite

作用

相比较于java,GORM实现着与JDBC、MyBatis等驱动的功能,能够在golang的环境内对数据库进行操作,能实现增删改查,同时也支持Hook、事务等功能。

image.png

操作

type User struct {
  ID           uint
  Name         string
  Email        *string
  Age          uint8
  Birthday     *time.Time
  MemberNumber sql.NullString
  ActivatedAt  sql.NullTime
  CreatedAt    time.Time
  UpdatedAt    time.Time
}


func (p User) TableName() string{
return "user" 
}
//创建一个结构体User,结构体User对应表名为user

连接数据库

func main() {
  // 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
  dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
  db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
}

增加

user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()} 
result := db.Create(&user) // 通过数据的指针来创建
user.ID             // 返回插入数据的主键 
result.Error        // 返回 error
result.RowsAffected // 返回插入记录的条数
批量增加
var users = []User{{Name: "jinzhu1"}, {Name: "jinzhu2"}, {Name: "jinzhu3"}} 
db.Create(&users)  
for _, user := range users {
user.ID // 1,2,3 
}

查询

// 获取第一条记录(主键升序)  
db.First(&user)  
// SELECT * FROM users ORDER BY id LIMIT 1;  

// 获取一条记录,没有指定排序字段  
db.Take(&user)  
// SELECT * FROM users LIMIT 1;  

// 获取最后一条记录(主键降序)  
db.Last(&user)  
// SELECT * FROM users ORDER BY id DESC LIMIT 1;  

result := db.First(&user)  
result.RowsAffected // 返回找到的记录数  
result.Error // returns error or nil  

// 检查 ErrRecordNotFound 错误  
errors.Is(result.Error, gorm.ErrRecordNotFound)

若想避免ErrRecordNotFound错误,你可以使用Find,比如 db.Limit(1).Find(&user)Find方法可以接受struct和slice的数据。

带有条件的查询,如下图 image.png

删除

删除一条记录时,删除对象需要指定主键,否则会触发批量 Delete,例如:

// 删除Email 的 ID 是 `10` 的记录
db.Delete(&email) 
// DELETE from emails where id = 10;

// 带额外条件的删除 
db.Where("name = ?", "jinzhu").Delete(&email) 
// DELETE from emails where id = 10 AND name = "jinzhu"; 

根据主键删除

db.Delete(&User{}, 10) 
// DELETE FROM users WHERE id = 10; 
db.Delete(&User{}, "10")
// DELETE FROM users WHERE id = 10;

更新

更新单个列

// 条件更新
db.Model(&User{}).Where("active = ?", true).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE active=true;

// User 的 ID 是 `111`
db.Model(&user).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111;

更新多列

// 根据 `struct` 更新属性,只会更新非零值的字段 
db.Model(&user).Updates(User{Name: "hello", Age: 18, Active: false})
// UPDATE users SET name='hello', age=18, updated_at = '2013-11-17 21:34:10' WHERE id = 111; 

// 根据 `map` 更新属性 
db.Model(&user).Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false}) 
// UPDATE users SET name='hello', age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111; 

//**注意** 当使用 struct 进行更新时,GORM 只会更新非零值的字段。
//你可以使用 `map` 更新字段,或者使用 `Select` 指定要更新的字段

以上为最基本的GORM用法,更多使用方法请访问官网

Kitex

安装

go install github.com/cloudwego/kitex/tool/cmd/kitex@latest
go install github.com/cloudwego/thriftgo@latest

定义IDL

namespace go api

struct Request {
    1: string message
}

struct Response {
    1: string message
}

service Echo {
    Response echo(1: Request req)
}

如果我们要进行PRC,就需要知道对方的接口是什么,需要传什么参数,同时也需要知道返回值时什么样的,这个时候,就需要通过IDL来约定双方的协议,就像在写代码的时候需要调用某个函数,我们需要知道函数签名一样。

Kitex生成代码

kitex -module example -service example echo.thrift

生成后的目录:

image.png

  • build.sh :构建脚本
  • kitex_gen:IDL 内容相关的生成代码,主要是基础的Server/Client 代码。
  • main.go 程序入口
  • handler.go用户在该文件里实现IDLservice 定义的方法

使用

创建Client

服务默认监听8888端口

echo.NewClient 用于创建 client,其第一个参数为调用的 服务名,第二个参数为options,用于传入参数, 此处的 client.WithHostPorts 用于指定服务端的地址。

import "github.com/cloudwego/kitex/client"
...
c, err := echo.NewClient("example", client.WithHostPorts("0.0.0.0:8888"))
if err != nil {
log.Fatal(err)
}

服务注册与发现

Kitex 的服务注册与发现已经对接了主流了服务注册与发现中心,如 ETCD,Nacos 等。

   r, err := etcd.NewEtcdResolver([]string{"127.0.0.1:2379"})
   if err != nil {
   	log.Fatal(err)
   }
   client := hello.MustNewClient("Hello", client.WithResolver(r))
   for {
   	ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
   	resp, err := client.Echo(ctx, &api.Request{Message: "Hello"})
   	cancel()
   	if err != nil {
   		log.Fatal(err)
   	}
   	log.Println(resp)
   	time.Sleep(time.Second)
   }
}

Hertz

样例

//Hertz 提供了 GET、POST、PUT、DELETE、ANY 等方法用于注册路由。
package main

import (
	"context"
	"github.com/cloudwego/hertz/pkg/app"
	"github.com/cloudwego/hertz/pkg/app/server"
	"github.com/cloudwego/hertz/pkg/protocol/consts"
)

func main(){
	h := server.Default(server.WithHostPorts("127.0.0.1:8080"))

	h.StaticFS("/", &app.FS{Root: "./", GenerateIndexPages: true})

	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")
	})
	h.Spin()
}

路由分组

//Hertz 提供了参数路由和通配路由,路由的优先级为:静态路由 > 命名路由 > 通配路由
package main

import (
	"context"
	"github.com/cloudwego/hertz/pkg/app"
	"github.com/cloudwego/hertz/pkg/app/server"
	"github.com/cloudwego/hertz/pkg/protocol/consts"
)

func main(){
	h := server.Default(server.WithHostPorts("127.0.0.1:8080"))
	v1 := h.Group("/v1")
	v1.GET("/get", func(ctx context.Context, c *app.RequestContext) {
		c.String(consts.StatusOK, "get")
	})
	v1.POST("/post", func(ctx context.Context, c *app.RequestContext) {
		c.String(consts.StatusOK, "post")
	})
	v2 := h.Group("/v2")
	v2.PUT("/put", func(ctx context.Context, c *app.RequestContext) {
		c.String(consts.StatusOK, "put")
	})
	v2.DELETE("/delete", func(ctx context.Context, c *app.RequestContext) {
		c.String(consts.StatusOK, "delete")
	})
	h.Spin()
}

参数绑定和校验

Hertz 提供了 Bind、Validate、 BindAndValidate 函数用于进行参数绑定和校验。

func main() {
	r := server.New()

    r.GET("/hello", func(c context.Context, ctx *app.RequestContext) {
        // 参数绑定需要配合特定的go tag使用
		type Test struct {
            A string `query:"a" vd:"$!='Hertz'"`
        }

        // BindAndValidate
        var req Test
        err := ctx.BindAndValidate(&req)

        ...

	    // Bind
        req = Test{}
        err = ctx.Bind(&req)

        ...

        // Validate,需要使用 "vd" tag
        err = ctx.Validate(&req)

        ...
    })
...
}

中间件

分为两类:服务端中间件和客户端中间件

//此为服务端的中间件的实现

func MyMiddleware() app.HandlerFunc {
  return func(ctx context.Context, c *app.RequestContext) {
    // pre-handle
    // ...
    c.Next(ctx)
  }
}

Hertz提供了代码生成工具Hz,通过定义IDL文件即可生成对应的基础服务代码。

安装hz:
go install github.com/cloudwego/hertz/cmd/hz@latest
  • 用thrift IDL创建新项目,下面是thrift IDL文件:
// idl/hello.thrift
namespace go hello.example

struct HelloReq {
    1: string Name (api.query="name"); // 添加 api 注解为方便进行参数绑定
}

struct HelloResp {
    1: string RespBody;
}


service HelloService {
    HelloResp HelloMethod(1: HelloReq request) (api.get="/hello");
}

image.png

好难啊