如何写一手漂亮的代码 - Go

序言

参加 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
}

这里说一下我的考虑,在一些层面看来这段代码改动甚至不能算是优化,它存在两个问题:

  1. 代码的行数并没有减少,反而增多了。
  2. 在调用方法时生成了很多临时变量,在 GC 的时候会增加扫描负担,影响性能。

我的想法:

  1. 我认为一个好的优化,应该是在逻辑层看更简单易懂的逻辑,让代码看起来很清晰,减少重复性,将一些共性的东西抽象出来,其他位置有相同的逻辑可以进行复用,只要你的方法抽象合理。
  2. 我觉得这是一个个人取舍的问题,我对代码是有洁癖的,我不喜欢看到大量重复的恶心代码,哪怕会因此损失一小部分的性能,而且当服务的流量没有高到离谱的时候,这段代码对性能的影响微乎其微,如果真的到达了性能的瓶颈期,那是不是应该考虑下硬件资源是不是该加强下,或者架构上是否合理,当然很多人认为性能本身就是挤牙膏,那也无可厚非,至少我觉得写一手可读性高且美观的代码很重要,当然可能还有更好的方案,也欢迎沟通交流 …

Gorm 2.0 的一些新东西及注意事项

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

使用对比
  • Gorm 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
	}

	// 入库
}
  • Gorm 2.0 的代码

定义结构

// 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 会出现两种可能

  1. 数组中只有一个元素,存进去不是个数组而是对象,结果取出来的时候 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")
}
  1. 数组中有两个元素,执行发生报错 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 中如果 stringinttime.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 表,FindFirst 取值时使用 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 Receiver

习惯使用 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)
}
指定切片容量

时间相差 2 s +

  • 未指定容量
// 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")
}

TO BE CONTINUE …


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!