Go 学习笔记 - Gopher China

本文最后更新于:2023年4月25日 晚上

描述

参加 Gopher China 2020 感觉所获颇丰,简单整理一下,共勉 …

代码解耦

  • 旧代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    if len(req.FirstName) == 0 {
    msg.Code = ErrFirstNameIsRequired
    return
    }
    if len(req.LastName) == 0 {
    msg.Code = ErrLastNameIsRequired
    return
    }
    if len(req.Address) == 0 {
    msg.Code = ErrAddressIsRequired
    return
    }
    if len(req.PostalCode) == 0 {
    msg.Code = ErrPostalCodeIsRequired
    return
    }
    if len(req.City) == 0 {
    msg.Code = ErrCityIsRequired
    return
    }
  • 改动后

    抽象一个方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // ValidateString 字符串验证结构
    type ValidateString struct {
    Str string
    Code int
    }

    // ValidateEmptyString 验证字符串是否为空
    func ValidateEmptyString(s []*ValidateString) (code int, empty bool) {
    for _, v := range s {
    if len(v.Str) == 0 {
    empty = true
    code = v.Code
    return
    }
    }
    return
    }

    核心逻辑

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 参数合法性校验
    code, empty := tools.ValidateEmptyString([]*tools.ValidateString{
    {Str: req.FirstName, Code: ErrFirstNameIsRequired},
    {Str: req.LastName, Code: ErrLastNameIsRequired},
    {Str: req.Address, Code: ErrAddressIsRequired},
    {Str: req.PostalCode, Code: ErrPostalCodeIsRequired},
    {Str: req.City, Code: ErrCityIsRequired},
    })
    if empty {
    msg.Code = code
    return
    }
    说一下我的考虑,在一些层面看来这段代码改动甚至不能算是优化,它存在两个问题:
  1. 代码的行数并没有减少,反而增多了。

    我认为一个好的优化,应该是在逻辑层看更简单易懂的逻辑,让代码看起来很清晰,减少重复性,将一些共性的东西抽象出来,其他位置有相同的逻辑可以进行复用,只要你的方法抽象合理

  2. 在调用方法时生成了很多临时变量,在 GC 的时候会增加扫描负担,影响性能。

    我觉得这是一个个人取舍的问题,我对代码是有洁癖的,我不喜欢看到大量重复的恶心代码,哪怕会因此损失一小部分的性能,而且当服务的流量没有高到离谱的时候,这段代码对性能的影响微乎其微,如果真的到达了性能的瓶颈期,那是不是应该考虑下硬件资源是不是该加强下,或者架构上是否合理,当然很多人认为性能本身就是挤牙膏,那也无可厚非,至少我觉得写一手可读性高且美观的代码很重要,当然可能还有更好的方案,也欢迎沟通交流 …

    Gorm 2.0 的新东西及注意事项

    在我看来,在 Gorm 2.0 版本我们基本告别了 json.RawMessage 这个结构了、官方提供了自定义类型的方式,只需要实现两个方法:Scan 、 Value

一些对比

v1

定义结构

1
2
3
4
5
6
7
8
9
10
11
12
// Event 表
type Event struct {
ID string `gorm:"TYPE:TEXT;PRIMARY_KEY"` // ID
Info json.RawMessage `gorm:"TYPE:JSONB;DEFAULT:'{}'"` // 详细信息
CreatedAt time.Time `gorm:"DEFAULT:CURRENT_TIMESTAMP"` // 创建时间
UpdatedAt time.Time `gorm:"DEFAULT:CURRENT_TIMESTAMP"` // 更新时间
}

// Info 一些信息
type Info struct {
Detail string `json:"detail"`
}

抽象通用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// SetInfo 设置信息
func (e *Event) SetInfo(from string, info *Info) error {
newInfo, err := json.Marshal(info)
if err != nil {
logrus.Error(from+"SetInfo: ", err)
return err
}
e.Info = newInfo
return nil
}

// GetInfo 获取信息
func (e *Event) GetInfo(from string) *Info {
dbInfo := new(Info)
err := json.Unmarshal(e.Info, &dbInfo)
if err != nil {
logrus.Error(from+"GetInfo: ", err)
return nil
}
return dbInfo
}

落地使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func main() {
e := new(Event)
// 获取信息
info := e.GetInfo("main")
if info == nil || string(e.Info) == "{}" {
logrus.Errorf("The info is empty [ %s ]", e.ID)
return
}

// 新增信息
err := e.SetInfo("main", &Info{
Detail: "",
})
if err != nil {
logrus.Errorf("e.SetInfo: [ %v ]", err)
return
}

// 入库
}

v2

定义结构

1
2
3
4
5
6
7
8
9
10
11
12
// Example 示例表
type Example struct {
ID string `gorm:"TYPE:TEXT;PRIMARY_KEY"` // ID
Info Info `gorm:"TYPE:JSONB;DEFAULT:'{}'"` // 详细信息
CreatedAt time.Time `gorm:"DEFAULT:CURRENT_TIMESTAMP"` // 创建时间
UpdatedAt time.Time `gorm:"DEFAULT:CURRENT_TIMESTAMP"` // 更新时间
}

// Info 一些信息
type Info struct {
Detail string `json:"detail"`
}

接口实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Scan 查询实现
func (b *Info) Scan(value interface{}) error {
bytes, ok := value.([]byte)
if !ok {
return errors.New(fmt.Sprint("Failed to unmarshal JSONB value:", value))
}

result := Info{}
err := json.Unmarshal(bytes, &result)
*b = result
return err
}

// Value 存储实现
func (b Info) Value() (driver.Value, error) {
return json.Marshal(b)
}

落地使用

1
2
3
4
5
6
7
8
9
10
11
func main() {
e := new(Event)
// 获取信息:数据库查询处理出来就已经处理好了

// 新增信息
e.Info = Info{
Detail: "",
}

// 入库
}

可以看到,落地使用的代码变得非常简捷,个人认为 Gorm 2.0 的自定义类型使项目本身的代码更加细致,虽然还是用程序去做 JOSN 解析而不是让数据库去做,但是可用性已经有了很大的提升,而且我个人也不是很倾向于让数据库去做这件事情,尽管他本身支持

自定义类型注意

我们如果想定义一个 JSON 数组、Map 的话,我们的处理方式就要改变一下

定义结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Example struct {
ID string `gorm:"TYPE:TEXT;PRIMARY_KEY"` // ID
Info Infos `gorm:"TYPE:JSONB;DEFAULT:'{}'"` // 详细信息
CreatedAt time.Time `gorm:"DEFAULT:CURRENT_TIMESTAMP"` // 创建时间
UpdatedAt time.Time `gorm:"DEFAULT:CURRENT_TIMESTAMP"` // 更新时间
}

// Info 一些信息
type Info struct {
Detail string `json:"detail"`
}

// Infos 一组信息
type Infos []Info

接口实现都是一样的,但是存储的时候要注意一个问题,就是不能直接存数组,因为你定义的类型 Infos 程序是认识的,但是并不认识 []Info,所以如果你直接存 []info 会出现两种可能

  1. 数组中只有一个元素,存进去不是个数组而是对象,结果取出来的时候 JSON 反序列化失败。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // UpdateExample 测试更新示例
    func TestUpdateExample(t *testing.T) {
    id := "exampleID"
    is := []Info{
    {Detail: "example detail"},
    }

    err = UpdateExample(id, map[string]interface{}{
    "infos": is,
    })
    if err != nil {
    t.Fatal(err)
    }
    t.Log("Success")
    }
  2. 数组中有两个元素,执行发生报错 ERROR: column “infos” is of type jsonb but expression is of type record (SQLSTATE 42804)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // UpdateExample 测试更新示例
    func TestUpdateExample(t *testing.T) {
    id := "exampleID"
    is := []Info{
    {Detail: "example detail 1"},
    {Detail: "example detail 2"},
    }

    err = UpdateExample(id, map[string]interface{}{
    "infos": is,
    })
    if err != nil {
    t.Fatal(err)
    }
    t.Log("Success")
    }
    正确存储方式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // UpdateExample 测试更新示例
    func TestUpdateExample(t *testing.T) {
    id := "exampleID"
    var is = Infos{}
    is = []Info{
    {Detail: "example detail 1"},
    {Detail: "example detail 2"},
    }

    err = UpdateExample(id, map[string]interface{}{
    "infos": is,
    })
    if err != nil {
    t.Fatal(err)
    }
    t.Log("Success")
    }

你必须先显式的指定你所存储的变量是你自定义的数据类型,存储才会是一个数组,否则只会把数据解析成对应结构的对象存储入库

常规类型注意

在 Gorm 2.0 中如果 stringinttime.Time 等类型,字段默认是 NULL 的话,扫描的时候会报错:converting NULL to (string/int …) is unsupported

  • 解决方式一(使用已经定义好的数据库类型)

    注意:使用这种方式存储的时候 Valid 字段必须显式的给出 True 才会存储,不然就会存储一个 null。

    1
    2
    3
    4
    5
    6
    7
    8
    // Example 示例
    type Example struct {
    Str sql.NullString `gorm:"DEFAULT:NULL"` // 字符串
    Int sql.NullInt64 `gorm:"DEFAULT:NULL"` // 数字
    Bool sql.NullBool `gorm:"DEFAULT:NULL"` // 布尔
    Float sql.NullFloat64 `gorm:"DEFAULT:NULL"` // 浮点
    Time pq.NullTime `gorm:"DEFAULT:NULL"` // 时间
    }
    个人习惯这样使用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    var (
    str = "a string"
    i int64
    f float64
    t = time.Now()
    )

    e := &Example{
    Str: sql.NullString{String: str, Valid: len(str) > 0},
    Int: sql.NullInt64{Int64: i, Valid: true},
    Bool: sql.NullBool{Bool: true, Valid: true},
    Float: sql.NullFloat64{Float64: f, Valid: true},
    Time: pq.NullTime{Time: t, Valid: !t.IsZero()},
    }
  • 解决方式二:将默认值改为对应类型的零值

查询注意

在 Gorm 1.0 中我们可能会定义这样一种结构

1
2
3
4
5
6
// Example 示例
type Example struct {
Value sql.NullString `gorm:"DEFAULT:NULL"` // 表内字段

JoinValue string `gorm:"-"` // JOIN 表字段
}

在 Gorm 2.0 中 JoinValue 是不会查询到值的 - 这个标签被视为忽略读写,如果只期望查询而不存取的话现在应该使用内嵌,查询的时候还是查询 Example 表,FindFirst 取值时使用 SelectExample 取值就可以了。

1
2
3
4
5
6
7
8
9
10
11
// SelectExample 查询结构
type SelectExample struct {
Example Example `gorm:"embedded"` // 表内字段

JoinValue string `gorm:"->"` // JOIN 表字段
}

// Example 示例
type Example struct {
Value sql.NullString `gorm:"DEFAULT:NULL"` // 表内字段
}

Gopher China 会议学习整理

主要还是一些 陈皓 在 2020 会议上讲得一些东西

Function VS Receive

习惯使用 Receiver 的方式

  • Function
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    func PrintPerson(p *Person) {
    fmt.Printf("Name=%s, Sexual=%s, Age=%d\n",
    p.Name, p.Sexual, p.Age)
    }
    func main() {
    var p = Person{
    Name: "Hao Chen",
    Sexual: "Male",
    Age: 44}
    PrintPerson(&p)
    }
  • Receiver
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    func (p *Person) Print() {
    fmt.Printf("Name=%s, Sexual=%s, Age=%d\n",
    p.Name, p.Sexual, p.Age)
    }
    func main() {
    var p = Person{
    Name: "Hao Chen",
    Sexual: "Male",
    Age: 44}
    p.Print()
    }

共性方法

主要目的还是抽离共性代码,避免重复代码出现

  • 源码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    type Country struct {
    Name string
    }
    type City struct {
    Name string
    }
    type Printable interface {
    PrintStr()
    }

    func (c Country) PrintStr() {
    fmt.Println(c.Name)
    }
    func (c City) PrintStr() {
    fmt.Println(c.Name)
    }
    func main() {
    c1 := Country{"China"}
    c2 := City{"Beijing"}
    c1.PrintStr()
    c2.PrintStr()
    }
  • 优化
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    type WithName struct {
    Name string
    }
    type Country struct {
    WithName
    }
    type City struct {
    WithName
    }
    type Printable interface {
    PrintStr()
    }

    func (w WithName) PrintStr() {
    fmt.Println(w.Name)
    }

    func main() {
    c1 := Country{WithName{"China"}}
    c2 := City{WithName{"Beijing"}}
    c1.PrintStr()
    c2.PrintStr()
    }

验证接口是否被实现

  • 接口定义及实现
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    type Shape interface {
    Sides() int
    Area() int
    }
    type Square struct {
    len int
    }

    func (s *Square) Sides() int {
    return 4
    }
  • 验证
    1
    var _ Shape = (*Square)(nil)

    报错:cannot use (*Square)(nil) (type *Square) as type Shape in assignment: *Square does not implement Shape (missing Area method)

性能对比

尽量使用 strconv 而不是 fmt

时间相差 78 ns +

  • fmt
    1
    2
    3
    4
    // 143 ns/op
    for i := 0; i < b.N; i++ {
    s := fmt.Sprint(rand.Int())
    }
  • strconv
    1
    2
    3
    4
    // 64.2 ns/op
    for i := 0; i < b.N; i++ {
    s := strconv.Itoa(rand.Int())
    }

避免 string to byte 的转换

时间相差 18 ns +

1
2
3
4
// 22.2 ns/op
for i := 0; i < b.N; i++ {
w.Write([]byte("Hello world"))
}
1
2
3
4
5
// 3.25 ns/op
data := []byte("Hello world")
for i := 0; i < b.N; i++ {
w.Write(data)
}

指定切片容量

时间相差 18 ns +

  • 未指定容量
    1
    2
    3
    4
    5
    6
    7
    // 100000000 2.48s
    for n := 0; n < b.N; n++ {
    data := make([]int, 0)
    for k := 0; k < size; k++ {
    data = append(data, k)
    }
    }
  • 指定容量
    1
    2
    3
    4
    5
    6
    7
    // 100000000 0.21s
    for n := 0; n < b.N; n++ {
    data := make([]int, 0, size)
    for k := 0; k < size; k++ {
    data = append(data, k)
    }
    }

使用 StringBuffer 或者 StringBuilder

时间相差 12 ns + ,不过这个差距看起来还是很明显的

  • string +=
    1
    2
    3
    4
    5
    6
    // 12.7 ns/op
    var strLen int = 30000
    var str string
    for n := 0; n < strLen; n++ {
    str += "x"
    }
  • StringBuilder
    1
    2
    3
    4
    5
    6
    // 0.0265 ns/op
    var strLen int = 30000
    var builder strings.Builder
    for n := 0; n < strLen; n++ {
    builder.WriteString("x")
    }
  • StringBuffer
    1
    2
    3
    4
    5
    6
    // 0.0088 ns/op
    var strLen int = 30000
    var buffer bytes.Buffer
    for n := 0; n < strLen; n++ {
    buffer.WriteString("x")
    }

TO BE CONTINUE …


Go 学习笔记 - Gopher China
https://agopher.com/2020/12/03/tech/2020_go_gopherChina/
作者
冷宇生(Allen)
发布于
2020年12月3日
更新于
2023年4月25日
许可协议