GopherChina 2020 Go Programming Patterns 学习笔记篇1

今天学习的是左耳朵耗子老师的 Go Programming Patterns,包括Slice,深度比较,接口,多态,Time,性能以及委托模式和错误处理等话题。

Topic 1 Slice

我们知道Slice是一个结构体

type SliceHeader struct {
     
	Data uintptr
	Len  int
	Cap  int
}

一个var a []int 是nil,但是它的len和cap都将是零,因为值是初始化这个slice结构体的零值,即

SliceHeader{
     
	Data: nil,
	Len: 0,
	Cap: 0,
}

PPT里讨论的是slice的共享内存,在append时候,是否会reallocate。如下代码,分配一个32的长度的slice a,此时新建slice b指向a的1到15,然后改变a中index为2的值,这个会导致b变化吗?这里是不会的,因为a在append(a,1)时候已经reallocate了。a之所以会reallocate是因为make里指定的长度为32,cap也是32,这时候append会导致长度+1,cap不足,致使扩容,然后a就会被赋新的地址。而b依然指向的是之前的地址。

a := make([]int, 32)
a[2] = 41
b := a[1:16] // a[2] 和 b[1] 指向的index是同一个
a = append(a, 1)
a[2] = 42
fmt.Printf("a[2]=%d\n", a[2]) // 打印42
fmt.Printf("b[1]=%d", b[1])  // 打印41

何时会导致slice扩容呢,只有cap不够的时候。看以下代码,dir1的cap是多少呢?是path的长度,即14,那么dir2也是14吗?不是的,dir2的cap是从它的起始位置开始计算到指向的slice的末尾,也就是从9。那么此时向dir1 append 数据,只要不超过14,就不会导致slice扩容,也就不会memory reallocate,但是会影响dir2的内容,因为dir1和dir2共享path的memory。

path := []byte("AAAA/BBBBBBBBB")
sepIndex := bytes.IndexByte(path,'/')
dir1 := path[:sepIndex]
dir2 := path[sepIndex+1:]
fmt.Printf("dir1 cap=%d, dir2 cap=%d\n", cap(dir1), cap(dir2))
fmt.Println("dir1 =>",string(dir1)) //prints: dir1 => AAAA fmt.Println("dir2 =>",string(dir2)) //prints: dir2 => BBBBBBBBB
dir1 = append(dir1,"suffix"...)
fmt.Println("dir1 =>",string(dir1)) //prints: dir1 => AAAAsuffix
fmt.Println("dir2 =>",string(dir2)) //prints: dir2 => uffixBBBB

Topic 2 Deep Comparison

Golang中是否可比较,可参考这可能是最全的golang的"=="比较规则了吧。我们这里仅摘一部分关于可比较的类型

1,基本类型
整型,包括int,uint,int8,uint8,int16,uint16,int32,uint32,int64,uint64,byte,rune,uintptr等
浮点型,包括float32,float64
复数类型,包括complex64,complex128
字符串类型,string
布尔型,bool
2,复合类型
数组
struct结构体
3,引用类型
slice
map
channel
pointer or 引用类型
4,接口类型
io.Reader, io.Writer,error等

  • 基本类型的比较需要是同一个类型的,比如int8 和int是不能比较的,必须强转一下,这是由于go语言是一种强类型的静态编译语言。
  • 复合类型的比较,数组是比较长度和挨个元素挨个元素比较的,当然了前提是数组中的元素是可比较的。struct也是逐个字段逐个字段比较的,也要求struct里的所有字段都是可比较的。
  • 引用类型的比较,普通的引用类型,只需要看内存地址是否一致就好了。但是对于slice和map,它们都只能和nil比较。
  • func类型,不可比较。
  • 接口类型比较,接口类型interface{}其实也是一个结构体,分为eface和iface,但都是type和data组成。所以接口的比较,就是动态的type类型和data都相等。
type eface struct {
     
    _type *_type
    data  unsafe.Pointer
}

type iface struct {
     
	tab  *itab
	data unsafe.Pointer
}

type itab struct {
     
	inter  *interfacetype
	_type  *_type
	link   *itab
	hash   uint32 // copy of _type.hash. Used for type switches.
	bad    bool   // type does not implement interface
	inhash bool   // has this itab been added to hash?
	unused [2]byte
	fun    [1]uintptr // variable sized
}

有几种比较方式:

  1. []byte类型的slice,可用bytes.Equal()来比较。
  2. 使用reflect.DeepEqual(x, y interface{}),可对不可比较的struct,slice和map进行比较,原理就是将其转换为Value,然后使用反射得出他们的动态类型和动态值,依次比较。
  3. 使用github.com/google/go-cmp/cmp包来比较。

Topic 3 Function vs Receiver

感觉这部分不知道讲的啥,这里应该要关注Receiver是指针还是struct。如果是struct,则调用函数将不能打印出自身值。而指针类型的Receiver可以。

Topic 4 Interface

这部分就是java里的多态设计,在golang里如何做接口继承和默认接口实现,这部分其实在很多库里有用。比如redis的cmdable接口。但是这个接口设计,还得稍微绕点弯子才能理解,因为很多人会忽略这部分,这应该是由于使用golang容易写出过程式的代码,所以导致大部分人对golang的多态了解太浅。

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) 
}
c1 := Country {
     "China"}
c2 := City {
     "Beijing"}
c1.PrintStr()
c2.PrintStr()

上面的代码通过实现接口Printable接口,City和Country都需要单独实现PrintStr()。在java里我们可以通过父类实现该方法,然后子类继承父类即可获取该接口,或者是使用java8的默认接口。那么在golang里怎么做呢?他没有extend 也没有 implement等字段,它的多态有两种。

  • 显式的,子类父类,在struct里包含要继承的父类struct,可实现多继承。
  • 隐式的,实现接口,是一种契约式的,即只要你实现我这个interface里定义的接口 那么就表明它实现了上下文。

我们先用,子类继承父类的方法来实现,减少代码重复。

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) 
}
c1 := Country {
     WithName{
      "China"}}
c2 := City {
      WithName{
     "Beijing"}}
c1.PrintStr()
c2.PrintStr()

看到了吧,我们定义了一个新的struct WithName,方便理解,我们按照java里的方式,叫它父类。然后我们的CityCountry作为子类,继承了WithName,只是继承的方式可能比较奇怪,就是嵌入到子类struct里,这就是显式的extend。然后父类WithName 实现了PrintStr()方法,按照刚才讲的是方法隐式的implement。

这里可以看到初始化的时候有点mess,PPT里还有一种方式,这种方式我理解为代理模式。Country和City依然是各自隐式实现接口Stringable,然后新建了一个代理的函数PrintStr(p Stringable),函数里参数接受的是Stringable,这样就可以将实现接口Stringable的类Country和City就可以作为参数传递进去,然后调用该接口的方法。

type Country struct {
     
    Name string
}
type City struct {
     
    Name string 
}
type Stringable interface {
     
    ToString() string
}
func (c Country) ToString() string {
     
    return "Country = " + c.Name 
}
func (c City) ToString() string{
      
    return "City = " + c.Name
}
func PrintStr(p Stringable) {
     
    fmt.Println(p.ToString()) 
}
d1 := Country {
     "USA"}
d2 := City{
     "Los Angeles"}
PrintStr(d1)
PrintStr(d2)

golang的标准库里也有类似的用法

func ReadAll(r io.Reader) ([]byte, error) {
     
	return readAll(r, bytes.MinRead)
}

// io.Reader是接口,这样ioutil.ReadAll可以接收所有实现Reader接口的struct。
type Reader interface {
     
	Read(p []byte) (n int, err error)
}

记住面向接口编程

Topic 5 验证接口的兼容性

这是一种什么用法呢?var _ Shape = (*Square)(nil) 有点黑科技的感觉。咨询了dravness大佬,这是什么用法

Go 语言只会在赋值等时机触发接口的检查,这个语句能够让编译器在编译期间检查Sqaure 类型是否实现 Shape 接口,不过我一般会这么写var _ Shape = &Square{}

type Shape interface {
      
    Sides() int
    Area() int
}

type Square struct {
     
    len int
}

func (s* Square) Sides() int {
     
    return 4 
}

func main() {
     
    s := Square{
     len: 5}
    fmt.Printf("%d\n",s.Sides()) 
}

var _ Shape = (*Square)(nil)

Topic 6 Time

要一直使用time.Time 和 time.Duration来表示时间,

  • 命令行 flag支持通过time.ParseDuration 解析time.Duration
  • JSON 编解码也支持RFC 3339字符串表示的time.Time
  • SQL database/sql 也支持转换DATETIME和TIMESTAMP 到time.Time。
  • YAML 支持RFC 3339 字符串,
    如果不能使用time.Time,那就是用RFC 3339定义的时间字符串

Topic 7 性能

  • strconv.Itoa(rand.Int()) 要比fmt.Sprint(rand.Int()) 快一倍
  • 在已知slice的大小情况下,指定slice的容量,避免扩容
  • 避免string to byte的转换
  • 使用StringBuffer 或者 StringBuilder来评接字符串,比+性能要高4个数量级
  • 避免热点代码的内存分配,使用sync.Pool来重用对象。
  • 异步多IO操作,使用WaitGroup来同步
  • 倾向无锁代码,使用Atomic包
  • 使用buffered io,使用bufio.NewWriter / bufio.NewReader
  • 使用编译好的正则表达式来完成重复的匹配
  • 使用Protocol Buffers而不是JSON,因为JSON会使用反射,
  • map要使用int keys而不是string keys
// strconv和fmt.Sprint的比较
for i := 0; i < b.N; i++ {
     
    s := fmt.Sprint(rand.Int()) 
}
for i := 0; i < b.N; i++ {
     
    s := strconv.Itoa(rand.Int()) 
}

// 指定slice的容量的比较
for n := 0; n < b.N; n++ {
     
      data := make([]int, 0)
      for k := 0; k < size; k++{
     
          data = append(data, k)
      }
}
// 100000000   2.48s
for n := 0; n < b.N; n++ {
     
      data := make([]int, 0, size)
      for k := 0; k < size; k++{
     
          data = append(data, k)
      }
}
// 100000000   0.21s

// 避免string到byte的转换
for i := 0; i < b.N; i++ {
     
    w.Write([]byte("Hello world"))
} // 22.2ns/op
data := []byte("Hello world")
for i := 0; i < b.N; i++ {
     
    w.Write(data)
} // 3.25ns/op

// StringBuilder\StringBuffer与+的比较
ar strLen int = 30000
var str string
for n := 0; n < strLen; n++ {
      
    str += "x"
}  // 12.7 ns/op

var builder strings.Builder 
for n := 0; n < strLen; n++ {
     
    builder.WriteString("x") 
}
// 0.0265 ns/op

var buffer bytes.Buffer
for n := 0; n < strLen; n++ {
      
    buffer.WriteString("x")
}
// 0.0088 ns/op

Topic 8 委托模式 Delegation

总体来说,就是多态实现。有点像上面的接口使用,比如定义Widget(包含X,Y属性)和Label类(包含Widget和Text),然后在定义Painter和Clicker接口。其中Label实现Painter的接口,并内嵌Widget。这样就完成了基础控件Label,然后在此基础上就可以衍生出Button,内嵌Label,并复写Painter接口和Clicker接口,以及ListBox(内嵌Widget,实现Clicker接口)。这样子,Button继承了Label,Label继承了Widget,都实现了Painter接口,其中Button实现了Clicker接口。ListBox继承了Widget,并实现了Clicker接口。

Topic 9 错误处理

错误处理一直是golang里被许多人诟病的问题,代码里几乎都是错误处理。每一个函数调用都有可能需要处理错误。其实有些错误没必要返回给上层知晓,比如类型转换,返回零值即可。或者是包装错误到struct里,使用这个对象的err方法来判断。

你可能感兴趣的:(Golang)