今天学习的是左耳朵耗子老师的 Go Programming Patterns,包括Slice,深度比较,接口,多态,Time,性能以及委托模式和错误处理等话题。
我们知道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
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等
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
}
有几种比较方式:
感觉这部分不知道讲的啥,这里应该要关注Receiver是指针还是struct。如果是struct,则调用函数将不能打印出自身值。而指针类型的Receiver可以。
这部分就是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等字段,它的多态有两种。
我们先用,子类继承父类的方法来实现,减少代码重复。
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里的方式,叫它父类。然后我们的City
和Country
作为子类,继承了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)
}
记住面向接口编程
这是一种什么用法呢?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)
要一直使用time.Time 和 time.Duration来表示时间,
// 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
总体来说,就是多态实现。有点像上面的接口使用,比如定义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接口。
错误处理一直是golang里被许多人诟病的问题,代码里几乎都是错误处理。每一个函数调用都有可能需要处理错误。其实有些错误没必要返回给上层知晓,比如类型转换,返回零值即可。或者是包装错误到struct里,使用这个对象的err方法来判断。