学习笔记 - 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
20if 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
}
- 代码的行数并没有减少,反而增多了。
我认为一个好的优化,应该是在逻辑层看更简单易懂的逻辑,让代码看起来很清晰,减少重复性,将一些共性的东西抽象出来,其他位置有相同的逻辑可以进行复用,只要你的方法抽象合理
- 在调用方法时生成了很多临时变量,在
GC
的时候会增加扫描负担,影响性能。我觉得这是一个个人取舍的问题,我对代码是有洁癖的,我不喜欢看到大量重复的恶心代码,哪怕会因此损失一小部分的性能,而且当服务的流量没有高到离谱的时候,这段代码对性能的影响微乎其微,如果真的到达了性能的瓶颈期,那是不是应该考虑下硬件资源是不是该加强下,或者架构上是否合理,当然很多人认为性能本身就是挤牙膏,那也无可厚非,至少我觉得写一手可读性高且美观的代码很重要,当然可能还有更好的方案,也欢迎沟通交流 …
Gorm 2.0 的新东西及注意事项
在我看来,在 Gorm 2.0 版本我们基本告别了 json.RawMessage 这个结构了、官方提供了自定义类型的方式,只需要实现两个方法:Scan 、 Value
一些对比
v1
定义结构
1 |
|
抽象通用方法
1 |
|
落地使用
1 |
|
v2
定义结构
1 |
|
接口实现
1 |
|
落地使用
1 |
|
可以看到,落地使用的代码变得非常简捷,个人认为 Gorm 2.0 的自定义类型使项目本身的代码更加细致,虽然还是用程序去做 JOSN 解析而不是让数据库去做,但是可用性已经有了很大的提升,而且我个人也不是很倾向于让数据库去做这件事情,尽管他本身支持
自定义类型注意
我们如果想定义一个 JSON 数组、Map 的话,我们的处理方式就要改变一下
定义结构
1 |
|
接口实现都是一样的,但是存储的时候要注意一个问题,就是不能直接存数组,因为你定义的类型 Infos
程序是认识的,但是并不认识 []Info
,所以如果你直接存 []info
会出现两种可能
- 数组中只有一个元素,存进去不是个数组而是对象,结果取出来的时候 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")
} - 数组中有两个元素,执行发生报错 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 中如果 string
、int
、time.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
14var (
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 |
|
在 Gorm 2.0 中 JoinValue
是不会查询到值的 -
这个标签被视为忽略读写,如果只期望查询而不存取的话现在应该使用内嵌,查询的时候还是查询 Example
表,Find
或 First
取值时使用 SelectExample
取值就可以了。
1 |
|
Gopher China 会议学习整理
主要还是一些 陈皓 在 2020 会议上讲得一些东西
Function VS Receive
习惯使用 Receiver 的方式
- Function
1
2
3
4
5
6
7
8
9
10
11func 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
11func (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
22type 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
23type 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
11type 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 |
|
1 |
|
指定切片容量
时间相差 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")
}