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方法来判断。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Go Design Patterns by Mario Castro Contreras English | 6 Mar. 2017 | ISBN: 1786466201 | 399 Pages | EPUB/PDF (conv) | 3.11 MB Key Features Introduction of the CSP concurrency model by explaining GoRoutines and channels. Objective comparison, with the help of images, of the CSP concurrency model and the Actor model to give the audience a "side by side" understanding of the CSP model. Explanation, including comprehensive text and examples, of all known GoF design patterns in Go. Book Description Go is a multi-paradigm programming language that has built-in facilities to create concurrent applications. Design patterns allow developers to efficiently address common problems faced during developing applications. Go Design Patterns will provide readers with a reference point to software design patterns and CSP concurrency design patterns to help them build applications in a more idiomatic, robust, and convenient way in Go. The book starts with a brief introduction to Go programming essentials and quickly moves on to explain the idea behind the creation of design patterns and how they appeared in the 90's as a common "language" between developers to solve common tasks in object-oriented programming languages. You will then learn how to apply the 23 GoF design patterns in Go and also learn about CSP concurrency patterns, the "killer feature" in Go that has helped Google develop software to maintain thousands of servers. Thus the book will enable you to understand and apply design patterns in an idiomatic way that will produce concise, readable, and maintainable software. What you will learn All basic syntax and tools needed to start coding in Go. Encapsulate the creation of complex objects in an idiomatic way in Go. Create unique instances that cannot be duplicated within a program. Understand the importance of object encapsulation to provide clarity and maintainability. Prepare cost-effective actions so that different parts of the program aren't affected by expens
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值