《GO语言圣经》学习笔记(七)方法和接口

知识点

接收器

func (p *Point) ScaleBy(factor float64) {
    p.X *= factor
    p.Y *= factor
}

此处 *Point 即是此函数的接收器

tips:
1、不管你的method的receiver是指针类型还是非指针类型,都是可以通过指针/非指针类型进行调用的,编译器会帮你做类型转换。
2、在声明一个method的receiver该是指针还是非指针类型时,你需要考虑两方面的内部,
第一方面是这个对象本身是不是特别大,如果声明为非指针变量时,调用会产生一次拷贝;
第二方面是如果你用指针类型作为receiver,那么你一定要注意,这种指针类型指向的始终是一块内存地址,就算你对其进行了拷贝。熟悉C或者C++的人这里应该很快能明白。

Nil也是一个合法的接收器类型

// An IntList is a linked list of integers.
// A nil *IntList represents the empty list.
type IntList struct {
    Value int
    Tail  *IntList
}
// Sum returns the sum of the list elements.
func (list *IntList) Sum() int {
    if list == nil {
        return 0
    }
    return list.Value + list.Tail.Sum()
}

顺便提一句,要小心空map,注意不要赋值,不然就恭喜get一个panic小技巧了
panic: assignment to entry in nil map

封装

Go语言只有一种控制可见性的手段:大写首字母的标识符会从定义它们的包中被导出,小写字母的则不会。这种限制包内成员的方式同样适用于struct或者一个类型的方法。因而如果我们想要封装一个对象,我们必须将其定义为一个struct。

封装的优点:

  • 首先,因为调用方不能直接修改对象的变量值,其只需要关注少量的语句并且只要弄懂少量变量的可能的值即可。
  • 第二,隐藏实现的细节,可以防止调用方依赖那些可能变化的具体实现,这样使设计包的程序员在不破坏对外的api情况下能得到更大的自由。
  • 封装的第三个优点也是最重要的优点,是阻止了外部调用方对对象内部的值任意地进行修改

接口

  • 看到一个接口类型的值,我们不能知道它是什么,唯一知道的就是可以通过它的方法来做什么。
  • 一个类型可以自由的使用另一个满足相同接口的类型来进行替换被称作可替换性(LSP里氏替换)
  • 一个类型如果拥有一个接口需要的所有方法,那么这个类型就实现了这个接口。鸭子类型
  • 空接口类型对实现它的类型没有要求,所以我们可以将任意一个值赋给空接口类型。
var any interface{}
any = true
any = 12.34
any = "hello"
any = map[string]int{"one": 1}
any = new(bytes.Buffer)

类型断言

类型断言是一个使用在接口值上的操作。语法上它看起来像x.(T)被称为断言类型,这里x表示一个接口的类型和T表示一个类型。一个类型断言检查它操作对象的动态类型是否和断言的类型匹配。

经常地我们对一个接口值的动态类型是不确定的,并且我们更愿意去检验它是否是一些特定的类型。如果类型断言出现在一个预期有两个结果的赋值操作中,例如如下的定义,这个操作不会在失败的时候发生panic但是代替地返回一个额外的第二个结果,这个结果是一个标识成功的布尔值:

var w io.Writer = os.Stdout
f, ok := w.(*os.File)      // success:  ok, f == os.Stdout
b, ok := w.(*bytes.Buffer) // failure: !ok, b == nil

第二个结果常规地赋值给一个命名为ok的变量。如果这个操作失败了,那么ok就是false值,第一个结果等于被断言类型的零值,在这个例子中就是一个nil的*bytes.Buffer类型。

这个ok结果经常立即用于决定程序下面做什么。if语句的扩展格式让这个变的很简洁:

if f, ok := w.(*os.File); ok {
    // ...use f...
}

当类型断言的操作对象是一个变量,你有时会看见原来的变量名重用而不是声明一个新的本地变量,这个重用的变量会覆盖原来的值,如下面这样:

if w, ok := w.(*os.File); ok {
    // ...use w...
}

基于类型断言区别错误类型

I/O可以因为任何数量的原因失败,但是有三种经常的错误必须进行不同的处理:文件已经存在(对于创建操作),找不到文件(对于读取操作),和权限拒绝。os包中提供了这三个帮助函数来对给定的错误值表示的失败进行分类:

package os

func IsExist(err error) bool
func IsNotExist(err error) bool
func IsPermission(err error) bool

eg.下面是IsNotExist的实际使用:

_, err := os.Open("/no/such/file")
fmt.Println(os.IsNotExist(err)) // "true"

类型开关

一个类型开关像普通的switch语句一样,它的运算对象是x.(type)-它使用了关键词字面量type-并且每个case有一到多个类型。一个类型开关基于这个接口值的动态类型使一个多路分支有效。这个nil的case和if x == nil匹配,并且这个default的case和如果其它case都不匹配的情况匹配。一个对sqlQuote的类型开关可能会有这些case:

switch x.(type) {
    case nil:       // ...
    case int, uint: // ...
    case bool:      // ...
    case string:    // ...
    default:        // ...
}

引用

拓展

[]byte 扩容算法

发现很多设计都是类似的实现,比如redis的SDS

type Buffer struct {
    buf     []byte
    initial [64]byte
    /* ... */
}

// Grow expands the buffer's capacity, if necessary,
// to guarantee space for another n bytes. [...]
func (b *Buffer) Grow(n int) {
    if b.buf == nil {
        b.buf = b.initial[:0] // use preallocated space initially
    }
    if len(b.buf)+n > cap(b.buf) {
        buf := make([]byte, b.Len(), 2*cap(b.buf) + n)
        copy(buf, b.buf)
        b.buf = buf
    }
}

欢迎大家关注我的公众号


半亩房顶

推荐阅读更多精彩内容