字节训练营学习记录

本篇文章主要记录在青训营X豆包MarsCode 技术训练营后端go语言学习的一些经历与过程。

一、青训营X豆包MarsCode 技术训练营之简易发帖项目记录

​ 首先就是这次文章是怎么来的,是因为在训练营中看到了第四课,讲的是Go语言工程实践以及对应使用的gin框架,但是发现项目全套代码是有了,但是却没有分段的对应代码,应该是后边课程讲的但是没在此次训练营的课程表中吗,所以在此也是根据自己的理解对该项目进行一定的记录学习。并且,由于是直接对于一个项目进行分析的,就不过多对于理论部分进行讲解了。

1、Gin框架。

​ gin框架也是go中的一个最常见的框架了,能够让我们快速的启动一些http服务,非常的方便快捷。这里也就不多介绍了,本篇文章的侧重点是项目分析,这个也不是很难,可以直接看官方文档即可。https://gin-gonic.com/zh-cn/docs/quickstart/

2、项目结构

首先就是我们可以去分析一下项目的结果,这里我将给的项目中的一些测试以及知识点代码都去掉了,剩下了这些代码,并且其中service是用来测试项目功能的

image-20241110123533028

接下来我们就是对这些代码进行理解。

首先就是启动项,server.go

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
46
47
48
49
50
51
52
53
54
55
package main

import (
"gopkg.in/gin-gonic/gin.v1"
"os"
//"github.com/Moonlight-Zhao/go-project-example/handler"
//"github.com/Moonlight-Zhao/go-project-example/repository"
"test01/server/handler"
"test01/server/repository"
"test01/server/util"
)

func main() {
if err := Init(); err != nil {
os.Exit(-1)
}
r := gin.Default()

r.Use(gin.Logger())

r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})

r.GET("/community/page/get/:id", func(c *gin.Context) {
topicId := c.Param("id")
data := handler.QueryPageInfo(topicId)
c.JSON(200, data)
})

r.POST("/community/post/do", func(c *gin.Context) {
uid, _ := c.GetPostForm("uid")
topicId, _ := c.GetPostForm("topic_id")
content, _ := c.GetPostForm("content")
data := handler.PublishPost(uid, topicId, content)
c.JSON(200, data)
})

err := r.Run(":8081")
if err != nil {
return
}
}

func Init() error {
if err := repository.Init(); err != nil {
return err
}
if err := util.InitLogger(); err != nil {
return err
}
return nil
}

我们就是可以发现这是一个很简单的启动服务

定义了三个路由,分别是一个测试路由,一个获取页面,一个添加页面的路由,没有什么很难理解的,Init则是初始化各组件,并且如果初始化失败则会返回err

接下来我们去看看util,发现里面只有一个文件,看名字就很浅显易懂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package util

import "go.uber.org/zap"

var Logger *zap.Logger

func InitLogger() error {
var err error
Logger, err = zap.NewProduction()
if err != nil {
return err
}
return nil
}

创建一个日志记录器方便使用,以方便调试

然后就是看repository,这个名字也很明显,仓库对吧,这也就是和数据库打交道的地方,专门用于操作数据库的,这里我们可以首先看数据库的初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package repository

import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)

var db *gorm.DB

func Init() error {
var err error
dsn := "root:root@tcp(127.0.0.1:3306)/community?charset=utf8mb4&parseTime=True&loc=Local"
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})

return err
}

很简单的一个Init函数,用于初始化数据库

接下来就是对应的三个文件分别对应数据库中的三个表

image-20241110130119241

image-20241110130127895

很容易看懂的结构也是,我们就对此,分析一个post文件吧

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package repository

import (
"gorm.io/gorm"
"sync"
"test01/server/util"
"time"
)

type Post struct {
Id int64 `gorm:"column:id"`
ParentId int64 `gorm:"column:parent_id"`
UserId int64 `gorm:"column:user_id"`
Content string `gorm:"column:content"`
DiggCount int64 `gorm:"column:digg_count"`
CreateTime time.Time `gorm:"column:create_time"`
}

func (Post) TableName() string { return "post" }

type PostDao struct{}

var postDao *PostDao
var postOnce sync.Once

func NewPostDaoInstance() *PostDao {
postOnce.Do(func() {
postDao = &PostDao{}
})
return postDao
}

func (*PostDao) QueryPostById(id int64) (*Post, error) {
var post Post
err := db.Where("id=?", id).Find(&post).Error
if err == gorm.ErrRecordNotFound {
return nil, nil
}
if err != nil {
util.Logger.Error("find post by id err:" + err.Error())
return nil, err
}

return &post, nil
}

func (*PostDao) QueryPostByParentId(parentId int64) ([]*Post, error) {
var posts []*Post
err := db.Where("parent_id = ?", parentId).Find(&posts).Error
if err != nil {
util.Logger.Error("find posts by parent_id err:" + err.Error())
return nil, err
}

return posts, nil
}

func (*PostDao) CreatePost(post *Post) error {
if err := db.Create(post).Error; err != nil {
util.Logger.Error("create post err:" + err.Error())
return err
}
return nil
}

首先就是创建了一个Post结构体去对应着表中的列,这样也就可以更好的去操作数据库

然后就是通过单例模式创建对应的数据库操作实例,从而避免了资源的浪费。

然后就是接下来的三个方法,就是QueryPostById通过id查询Post记录

QueryPostByParentId通过parentId查询多条post记录

CreatePost就是创建一条新的post

自此,我们就大概了解了如何通过go框架去操作数据库了

这时候也就剩下了最后一个文件夹,handler

这也就是处理程序的文件夹,用于处理对路由访问的并且返回对应所需结果的

我们可以看到有两个文件,启动publish_post就是将请求的队友结果返回

而query_page_info则是将对应的发布文章的请求进行处理并返回对应结果。

这里我们能够发现handler中的代码是调用了service层的方法的,这里我们也就可以剖析整个程序的流程了

server.go中的路由—–> handler——>service—–>repository

这也就是整个该项目的流程了

也就和java很像感觉。

最后就是对于service层的讲解

这里也就还是两个文件,对应handler中的两个操作

由service层调用数据库操作,从而获取到对应的数据,从而将其能够将数据返回。

自此,这个小项目的结构也就明了了,这也能够更加方便我们之后自己去开发项目,也要有着对应的结构,这样才能更好的去快速编写对应的功能。

3、项目总结

最后就是对整个项目的总结,这里我们了解了gin框架,并且完成了第一个go语言项目的编写,我们需要详细明白对应的项目结构,有着清晰的项目结构,才能更好的保障我们代码的可读性,还是gin框架,我感觉还是算是比较简单的,但是对于其操作数据库来说,我感觉稍微有点麻烦,毕竟对于每一个表都需要一个go文件与其对应才能使用,这样感觉有点啰嗦了。当然也不是说不好,只是个人这么感觉而已。
最后就是类比go语言和java,我感觉go语言中特别多的就是对于错误的处理,经常需要使用err去接受错误,并且去判断错误,以及打印,这点稍微有点麻烦,不过也有好处,就是能够快捷的指导自己代码哪里有错误什么的,方便去进行修改。

二、Gorm/Kitex/Hertz讲解

1、Gorm框架

这也是我们上面的小项目中所涉及使用到的框架,是用于操作数据库的一个框架。

这里附上一个GORM框架的中文文档https://gorm.io/zh_CN/docs/index.html

安装的话就不过多讲了,自己看文档都是有的

2、链接初始化

1
2
3
// 参考 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{})

通过设置dsn,去打开数据库,而且也能够在后边对Config进行相应的数据库连接设置。(包括一些基本设置,以及可以自定义对应的数据库驱动等等)

3、CRUD接口

完成了简单的数据库的初始化之后,我们就可以对数据库进行对应的一系列的操作了,Gorm支持所有对数据库的操作,这里我们也就一一简单介绍一下。

前置

这里要注意的是,使用Gorm对于数据库的操作,针对每个表,我们都需要有个对应的模型结构体去对应数据库的表以及其中的字段。

通俗来说,就是对于数据库操作中,我们习惯性创建一个repository文件夹,启动放置对应数据库有多少表就有多少个文件,一一对应着操作每个表。

并且每个文件中都要定义对应的结构体,也就是如下图所示

1
2
3
4
5
6
7
// gorm.Model 的定义
type Model struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}

这里注意,如果不使用gorm:"xxxx"去对字段进行申明,结构体字段名称转换为 snake_case 作为数据库中的列名

以及对应表名,我们可以使用Tablename方法去设置func (Post) TableName() string { return “post” },也可以使用默认的所对应命名,也就是将结构体名称转换为 snake_case 并为表名加上复数形式。 例如,一个 User 结构体在数据库中的表名变为 users

并且使用Gorm框架的情况下,我们还能去包含一些特定的字段,实现更好的功能

  • ID :每个记录的唯一标识符(主键)。
  • CreatedAt :在创建记录时自动设置为当前时间。
  • UpdatedAt:每当记录更新时,自动更新为当前时间。
  • DeletedAt:用于软删除(将记录标记为已删除,而实际上并未从数据库中删除)。

基础使用的话这些已经足够了,至于更加高级的,就可以去自己查看官方文档即可。

1、创建

直接使用上面返回的数据库,使用结构体的指针方式去创建对应的数据

1
2
user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()}
result := db.Create(&user) // 通过数据的指针来创建

这里一定要注意,传入的是结构体的指针,而不是单纯的结构体

用着指定的字段创建记录

创建记录并为指定字段赋值。

1
db.Select("Name", "Age", "CreatedAt").Create(&user)

创建记录并忽略传递给 ‘Omit’ 的字段值

1
db.Omit("Name", "Age", "CreatedAt").Create(&user)

批量插入

这里批量插入我们使用传入一个切片去完成,或者也可以使用map去创建也是可以的

1
2
var users = []User{{Name: "jinzhu1"}, {Name: "jinzhu2"}, {Name: "jinzhu3"}}
db.Create(&users)
1
2
3
db.Model(&User{}).Create(map[string]interface{}{
"Name": "jinzhu", "Age": 18,
})

2、查询

GORM 提供了 FirstTakeLast 方法,以便从数据库中检索单个对象。当查询数据库时它添加了 LIMIT 1 条件,且没有找到记录时,它会返回 ErrRecordNotFound 错误

image-20241112174939548

1
2
如果你想避免ErrRecordNotFound错误,你可以使用Find,比如db.Limit(1).Find(&user),Find方法可以接受struct和slice的数据。
建议就是尽量使用find而不是first以避免出现一些错误。

根据主键检索

如果主键是数字类型,您可以使用 内联条件 来检索对象。

1
2
3
4
5
6
db.First(&user, 10)
// SELECT * FROM users WHERE id = 10;
db.First(&user, "10")
// SELECT * FROM users WHERE id = 10;
db.Find(&users, []int{1,2,3})
// SELECT * FROM users WHERE id IN (1,2,3);

如果主键是字符串(例如像uuid这种),查询将会被写成如下

1
2
db.First(&user, "id = ?", "1b74413f-f3b8-409f-ac47-e8c062e3472a")
// SELECT * FROM users WHERE id = "1b74413f-f3b8-409f-ac47-e8c062e3472a";

检索全部对象

直接通过find去查找即可,这里要注意的是一下的部分

1
2
result.RowsAffected // returns found records count, equals `len(users)`
result.Error // returns error

通过条件检索

String条件

使用Where方法即可

image-20241112180255808

通过Struct&Map检索

可以通过结构体和Map去达到匹配多项的条件查询的目的

image-20241112180751008

但这里有一点要注意的就是,使用结构体查询是不会匹配空字符串或者0的,会进行忽略,只有使用map的话才能完成对应的匹配

1
2
3
4
5
db.Where(&User{Name: "jinzhu", Age: 0}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu";

db.Where(map[string]interface{}{"Name": "jinzhu", "Age": 0}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 0;

3、更新

保存所有字段

Save是也贵组合函数。如果保存值不包含主键,它将执行 Create,否则它将执行 Update (包含所有字段)。

1
2
3
4
5
db.Save(&User{Name: "jinzhu", Age: 100})
// INSERT INTO `users` (`name`,`age`,`birthday`,`update_at`) VALUES ("jinzhu",100,"0000-00-00 00:00:00","0000-00-00 00:00:00")

db.Save(&User{ID: 1, Name: "jinzhu", Age: 100})
// UPDATE `users` SET `name`="jinzhu",`age`=100,`birthday`="0000-00-00 00:00:00",`update_at`="0000-00-00 00:00:00" WHERE `id` = 1

更新单个列

这就很简单了,直接使用update即可。

更新多个列

也是类似,使用结构体和map就行,能够进行多列的更新。

更新所选定的字段

这里可以通过加一些选择进行对应选定字段的更新。

4、删除

最后就是删除了,删除应该算是最简单的了

直接就是可以通过Delete去对某列进行删除。

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

5、原生SLQ的使用

使用Raw和Scan去指定SQL语句

1
2
var result Result
db.Raw("SELECT id, name, age FROM users WHERE id = ?", 3).Scan(&result)

Exec原生SQL

1
2
// Exec with SQL Expression
db.Exec("UPDATE users SET money = ? WHERE name = ?", gorm.Expr("money * ? + ?", 10000, 1), "jinzhu")

4、事务

禁用默认事务

为了确保数据一致性,GORM 会在事务里执行写入操作(创建、更新、删除)。如果没有这方面的要求,您可以在初始化时禁用它,这将获得大约 30%+ 性能提升。

可以通过SkipDefaultTransaction对应配置去关闭

手动事务

Gorm 支持直接调用事务控制方法(commit、rollback),例如:

1
2
3
4
5
6
7
8
9
// 开始事务
tx := db.Begin()
// 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db')
tx.Create(...)
// ...
// 遇到错误时回滚事务
tx.Rollback()
// 否则,提交事务
tx.Commit()

这样差不多,Gorm的使用我们大致也就了解了,接下来就是让我们进入Kitex的学习!

自动提交事务

Gorm提供了Tansaction方法用于自动提交事务,避免用户漏写Commit、Rollback

5、Gorm生态

GORM 代码生成工具 https://github.com/go-gorm/gen
GORM 分片库方案 https://github.com/go-gorm/sharding
GORM 手动索引 https://github.com/go-gorm/hints
GORM 乐观锁 https://github.com/go-gorm/optimisticlock
GORM 读写分离 https://github.com/go-gorm/dbresolver
GORM OpenTelemetry扩展 https://github.com/go-gorm/opentelemetry

关于更多的GORM用法可以查看Gorm的文档(https://gorm.cn)

6、Kitex

这里先给上一个学习链接,本次总结也是主要针对链接内容进行的https://www.cloudwego.io/zh/docs/kitex/getting-started/

首先这边就是要先从RPC开始。

RPC是什么?

RPC (Remote Procedure Call) ,即远程过程调用。通俗来讲,就是调用远端服务的某个方法,并获取到对应的响应。RPC 本质上定义了一种通信的流程,而具体的实现技术没有约束,核心需要解决的问题为序列化网络通信。如可以通过 gob/json/pb/thrift 来序列化和反序列化消息内容,通过 socket/http 来进行网络通信。只要客户端与服务端在这两方面达成共识,能够做到消息正确的解析接口即可。

个人理解就是和HTTP挺类似的作用,使用RPC去能够实现类似HTTP远程访问的效果

这里就还要讲到一个东西,也就是IDL,这个在RPC中十分的重要

IDC又是什么?

IDL 全称是 Interface Definition Language,接口定义语言。

如果我们要使用 RPC 进行调用,就需要知道对方的接口是什么,需要传什么参数,同时也需要知道返回值是什么样的,就好比两个人之间交流,需要保证在说的是同一个语言、同一件事。IDL 就是为了解决这样的问题,通过 IDL 来约定双方的协议,就像在写代码的时候需要调用某个函数,我们需要知道 签名一样。

对于 RPC 框架,IDL 不仅作为接口描述语言,还会根据 IDL 文件生成指定语言的接口定义模块,这样极大简化了开发工作。服务提供方(服务端)需要做的变为 编写 IDL -> 使用代码生成工具生成代码 -> 实现接口;服务调用方(客户端)只需根据服务提供方(服务端)提供的 IDL 生成代码后进行调用。这当中还有服务发现、负载均衡等问题,但不属于 IDL 范畴,故不展开介绍。

Kitex 默认支持 thriftproto3 两种 IDL。

对应的语法参考链接:

Thrift IDL 语法可参考:Thrift interface description language

proto3 语法可参考:Language Guide(proto3)

对于Kitex框架的知识点,短时间内页无法讲解清楚,强雷建议就是去看看上面官网中的那个小案例,能够理解明白很多。

7、Kitex生态

XDS 扩展 https://github.com/kitex-contrib/xds
opentelemetry 扩展 https://github.com/kitex-contrib/obs-opentelemetry
ETCD 服务注册与发现扩展 https://github.com/kitex-contrib/registry-etcd
Nacos 服务注册与发现扩展 https://github.com/kitex-contrib/registry-nacos
Zookeeper 服务注册与发现扩展 https://github.com/kitex-contrib/registry-zookeeper
polaris 扩展 https://github.com/kitex-contrib/polaris
丰富的示例代码与业务Demo https://github.com/cloudwego/kitex-examples/

8、Hertz

简单介绍

Hertz[həːts] 是一个 Golang 微服务 HTTP 框架,在设计之初参考了其他开源框架 fasthttpginecho 的优势, 并结合字节跳动内部的需求,使其具有高易用性、高性能、高扩展性等特点,目前在字节跳动内部已广泛使用。 如今越来越多的微服务选择使用 Golang,如果对微服务性能有要求,又希望框架能够充分满足内部的可定制化需求,Hertz 会是一个不错的选择。

看看文档就很能明白其实和gin框架还是很相似的,但相比gin而言,Hertz有一个很好的点,就是有自动代码生成,也就是和上面的Kitex类似的IDL,使用IDL接口去自动生成对应代码,能够大大方便我们去编写对应的代码,十分的方便。

9、Hertz生态

HTTP2 扩展 https://github.com/hertz-contrib/http2
opentelemetry 扩展 https://github.com/hertz-contrib/obs-opentelemetry
国际化扩展 https://github.com/hertz-contrib/i18n
反向代理扩展 https://github.com/hertz-contrib/reverseproxy
JWT 鉴权扩展 https://github.com/hertz-contrib/jwt
Websocket 扩展 https://github.com/hertz-contrib/websocket
丰富的示例代码与业 Demo https://github.com/cloudwego/hertz-examples/

10、最后的小总结

个人感觉字节的这套框架很大程度的方便了程序员的代码编写,特别是加入了很多种类型的自动代码生成,避免了一些简单重复代码的多次编写,感觉这方面就非常好。而且通过这次的学习,我发现,学习还得是看官方文档,官方文档讲的很细很透彻,加上字节对于文档建设还是非常好的,视频的话更多的起到的是一个引导的作用,更多的还得是看自己的学习!