描述
参加 Gopher China 2020 感觉所获颇丰,简单整理一下,共勉 …
代码解耦
- 旧代码
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
}
- 改动后
抽象一个方法
// 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
}
核心逻辑
// 参数合法性校验
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
定义结构
// 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"`
}
抽象通用方法
// 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
}
落地使用
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
定义结构
// 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"`
}
接口实现
// 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)
}
落地使用
func main() {
	e := new(Event)
	// 获取信息:数据库查询处理出来就已经处理好了
	// 新增信息
	e.Info = Info{
		Detail: "",
	}
	// 入库
}
可以看到,落地使用的代码变得非常简捷,个人认为 Gorm 2.0 的自定义类型使项目本身的代码更加细致,虽然还是用程序去做 JOSN 解析而不是让数据库去做,但是可用性已经有了很大的提升,而且我个人也不是很倾向于让数据库去做这件事情,尽管他本身支持
自定义类型注意
我们如果想定义一个 JSON 数组、Map 的话,我们的处理方式就要改变一下
定义结构
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 会出现两种可能
- 数组中只有一个元素,存进去不是个数组而是对象,结果取出来的时候 JSON 反序列化失败。
// 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)
// 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")
}
正确存储方式
// 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。
// 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"` // 时间
}
个人习惯这样使用
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 中我们可能会定义这样一种结构
// Example 示例
type Example struct {
    Value sql.NullString `gorm:"DEFAULT:NULL"` // 表内字段
    JoinValue string `gorm:"-"` // JOIN 表字段
}
在 Gorm 2.0 中 JoinValue 是不会查询到值的 - 这个标签被视为忽略读写,如果只期望查询而不存取的话现在应该使用内嵌,查询的时候还是查询 Example 表,Find 或 First 取值时使用 SelectExample 取值就可以了。
// 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
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
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()
}
共性方法
主要目的还是抽离共性代码,避免重复代码出现
- 源码
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()
}
- 优化
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()
}
验证接口是否被实现
- 接口定义及实现
type Shape interface {
    Sides() int
    Area() int
}
type Square struct {
    len int
}
func (s *Square) Sides() int {
    return 4
}
- 验证
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
// 143 ns/op
for i := 0; i < b.N; i++ {
	s := fmt.Sprint(rand.Int())
}
- strconv
// 64.2 ns/op
for i := 0; i < b.N; i++ {
	s := strconv.Itoa(rand.Int())
}
避免 string to byte 的转换
时间相差 18 ns +
// 22.2 ns/op
for i := 0; i < b.N; i++ {
	w.Write([]byte("Hello world"))
}
// 3.25 ns/op
data := []byte("Hello world")
for i := 0; i < b.N; i++ {
	w.Write(data)
}
指定切片容量
时间相差 18 ns +
- 未指定容量
// 100000000 2.48s
for n := 0; n < b.N; n++ {
	data := make([]int, 0)
	for k := 0; k < size; k++ {
		data = append(data, k)
	}
}
- 指定容量
// 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 +=
// 12.7 ns/op
var strLen int = 30000
var str string
for n := 0; n < strLen; n++ {
	str += "x"
}
- StringBuilder
// 0.0265 ns/op
var strLen int = 30000
var builder strings.Builder
for n := 0; n < strLen; n++ {
	builder.WriteString("x")
}
- StringBuffer
// 0.0088 ns/op
var strLen int = 30000
var buffer bytes.Buffer
for n := 0; n < strLen; n++ {
	buffer.WriteString("x")
}
