golang学习笔记5 | 青训营

66 阅读9分钟

警告:捕获迭代变量

var rmdirs []func()
for _, dir := range tempDirs() {
    os.MkdirAll(dir, 0755)
    rmdirs = append(rmdirs, func() {
        os.RemoveAll(dir)   // 不正确
    })
}

在上面循环里创建的所有函数变量共享相同的变量------一个可访问的存储位置,而不是固定的值。因此,dir变量的实际取值是最后一次迭代时的值并且所有的os.RemoveAll调用最终都试图删除同一个目录。

变长函数

变长函数被调用的时候可以有可变的参数列表。

  • 在参数列表最后的类型名称之前使用省略号...表示声明一个变长函数,调用这个函数的时候可以传递该类型任意数目的参数。
func sum(vals ...int) int {
    total = 0
    for _, val = range vals {
        total += val
    }
    return total
}
​
fmt.Println(sum())          // "0"
fmt.Println(sum(3))         // "3"
fmt.Println(sum(1, 2, 3))   // "6"
  • 变长函数通常用于格式化字符串。

延迟函数调用

  • defer就是一个普通的函数或方法调用,在调用之前加上关键字defer。函数和参数表达式会在语句执行时求值,但是无论是正常情况下,执行return语句或函数执行完毕,还是不正常的情况下,比如发生宕机,实际的调用推迟到包含defer语句的函数结束后才执行。defer语句没有限制使用次数;执行的时候伊调用defer语句顺序的倒序进行(大概意思是类似栈这样先进后出?)。
var mu sync.Mutex
var m = make(map[string]int)
func lookup(key string) int {
    mu.Lock()
    defer mu.Unlock()
    return m[key]
}
  • 延迟执行的函数在return语句之后执行,并且可以更新函数的结果变量。因为匿名函数可以得到其外层的函数作用域内的变量(包括命名的结果),所以延迟执行的匿名函数可以观察到函数的返回结果。
  • 通过命名结果变量和增加defer语句,我们能够在每次调用函数的时候输出它的参数结果。
func double(x int) int {
    return x + x
}
func double(x int) (result int) {
    defer func() { fmt.Printf("double(%d) = %d\n", x, result) } ()
    return x + x
}
_ = double(4)
// 输出:
// "double(4) = 8"

这个技巧的使用相比之前的double函数来说有些过了,但对于有很多返回语句的函数来说很有帮助。

func triple(x int) (result int) {
    defer func() { result += x } ()
    return double(x)
}
​
fmt.Println(triple(4)) // "12"

宕机

golang的类型系统会捕获许多编译时错误,但有些其他的错误都需要在运行时进行检查。

  • goroutine中的所有延迟函数会执行,然后程序会异常退出并留下一条日志消息。日志消息包括宕机的值,这往往代表某种错误消息,每一个goroutine都会在宕机的时候显示一个函数调用的栈跟踪值。

恢复

  • 内置的recover函数在延迟函数的内部调用,而且这个包含defer语句的函数发生宕机,recover会终止当前的宕机状态并且返回宕机的值。函数不会从之前宕机的地方及继续运行而是正常返回。如果recover在其他任何情况下运行则它没有任何效果且返回nil
  • Parse函数中的延迟函数会从宕机状态恢复,并使用宕机值组成一条错误消息;理想的写法是使用runtime.Stack将整个调用栈包含进来。

方法

方法是某种特定类型的函数。

方法声明

方法的声明和普通函数的声明雷系,只是在函数名字前面多了一个参数。这个参数把这个方法绑定到这个参数对应的类型上。

  • 类型拥有的所有方法名都必须是唯一的,但不同的类型可以使用相同的方法名。
  • 命名类型与指向它们的指针是唯一可以出现在接收者声明处的类型。而且,为防止混淆,不允许本身是指针的类型进行方法声明。
  • 只有符合下面三种形式的语句才能够成立。
  1. 实参接收者和形参接受者是同一个类型,比如都是T类型或都是*T类型:
Point{1, 2}.Distance(q) // Point
pptor.ScaleBy(2)        // *Point
  1. 实参接收者是T类型的变量而形参接收者是*T类型。编译器会隐式地获取变量的地址:
p.ScaleBy(2)            // 隐式转换为(*pptr)

nil是一个合法的接收者

就像一些函数允许nil指针作为实参,方法的接收者也一样,尤其是当nil是类型中有意义的零值。

  • 当定义一个类型允许nil作为接收者时,应当在文档注释中显式地标明。

这是net/url包中Value类型的部分定义

package url
​
// Values 映射字符串到字符串列表
type Values map[string][] string// Get 返回第一个具有给定key的值
// 如不存在,则返回空字符串
func (v Values) Get(key string) string {
    if vs := v[key]; len(vs) > 0 {
        return vs[0]
    }
    return ""
}
​
// Add 添加一个键值到对应key列表中
func (v Values) Add(key, value string) {
    v[key] = append(v[key], value)
}

通过结构体内嵌组成类型

  • 匿名字段类型可以是个指向命名类型的指针,这个时候,字段和方法间接地来自于所指向的对象。这可以更加动态、多样化共享通用的结构以及对象关系。

方法变量与表达式

通常情况在相同的表达式里使用和调用方法,但是把两个操作分开也是可以的。选择子可以赋予一个方法变量,它是一个函数,把方法绑定到一个接收者上。函数只需要提供

  • 如果包内的API调用一个函数值,并且使用者期望这个函数的行为时调用一个特定接收者的方法,方法变量就非常有用。
type Rocket struct { /* ... */ }
func (r *Rocket) Launch() { /* ... */ }
​
r := new(Rocket)
time.AfterFunc(10 * time.Second, func() { r.Launch() }) --->
---> 可以简洁成 time.AfterFunc(10 * time.Second, r.Launch)
  • 与方法变量相关的是方法表达式。和调用一个普通的函数不同,在调用方法的时候必须提供接收者,并且按照选择子的语法进行调用。而方法表达式写成T.f或者(*T).f,其中T时类型,是一种函数变量,把原来方法的接收者替换成函数的第一个形参,英雌它可以像平常的函数一样调用。
p := Point{1, 2}
q := Point{4, 6}
​
distance := Point.Distance  // 方法表达式
fmt.Println(distance(p, q)) // "5"

如果需要用一个值来表达多个方法中的一个,而方法都属于同一类型,方法变量可以帮助调用这个值所对应的方法来处理不同的接收者。

封装

如果变量或者方法是不能通过对象访问到的,则称作封装的变量或方法。

  • golang只有一种方法控制命名的可见性:定义的时候,首字母大写的标识符是可以从包中导出的,而首字母没有大写的则不导出。同样的机制同样作用于结构体内的字段和类型中的方法。
  • 要封装一个对象,必须使用结构体。
  • golang封装的单元是包而不是类型。
  • 封装提供了三个优点:
  1. 不需要更多的语句检查变量的值(使用方不能直接修改对象的变量)。
  2. 隐藏实现细节可以防止使用方依赖的属性发生改变。
  3. 防止使用者肆意地改变对象内的变量。

接口

golang的接口的独特之处在于它是隐式实现的。

接口即约定

接口是一种抽象类型,它所提供的仅仅是一些方法而已。

  • fmt.Printffmt.Sprintf通过接口机制解决了重复部分问题。两个函数都封装了第三个函数fmt.Fprintf
package fmt
​
func Fprintf(w io.Writer, format string, args ...interface{}) (int, error)
​
func Printf(format string, args ...interface{}) string {
    var buf bytes.Buffer
    Fprintf(&buf, format, args...)
    return buf.String
}
​

Fprintf的第一个形参也不是文件类型,而是io.Writer接口类型,其声明如下:

package io
​
// Writer接口封装了基础的写入方法
type Writer interface {
    // Write 从p向底层数据流写入len(p)个字节的数据
    // 返回实际写入的字节数 (0 <= n <= len(p))
    // 如果没有写完,那么会返回遇到的错误
    // 在Write返回n<len(p)时,err必须为非nil
    // Write不允许修改p的数据,即使是临时修改
    
    // 实现时不允许残留p的引用
    Write(p []byte) (n int, err error)
}

io.Writer接口定义了Fprintf和调用者之间的约定。一方面,这个约定要求调用者提供的具体类型(比如*os.File或者*bytes.Buffer)包含一个与其签名和行为一致的Write方法。另一方面,这个约定保证了Fprintf能使用任何满足io.Writer接口的参数。

  • 可以把一种类型替换为满足同意接口的另一种类型的特征称为可取代性,这也是面向对象语言的典型特征。
  • 定义一个String方法就可以让类型满足这个广泛使用的接口fmt.Stringer
package fmt
​
// 在字符串格式化时如果需要一个字符串
// 那么就调用这个方法来把当前值转化为字符串
// Print这种不带格式化参数的输出方式也是调用这个方法
type Stringer interface {
    String() string
}

接口类型

一个接口类型定义了一套方法,如果一个具体类型要实现该接口,那么必须实现接口类型定义中的所有方法。

  • io包定义了很多有用的接口。io.Writer抽象了所有可以写入字节的类型的抽象;io.Reader抽象了所有可以读取字节的类型;io.Closer抽象了所有可以关闭的类型以及等等。
package io
​
type Reader interface {
    Read(p []byte) (n int, err error)
}
​
type Closer interface {
    Close() error
}

另外,还可以通过组合已有接口得到的新街口。

type ReadWriter interface {
    Reader
    Writer
}

如上语法称为嵌入式接口。

实现接口

接口的赋值规则仅当一个表达式实现了一个接口时,这个表达式才可以赋给该接口。

  • 非空的接口类型通常由一个指针类型来实现。
  • 指针类型肯定不是实现接口的唯一类型,即使是那些包含了会改变接受者方法的接口类型,也可以有golang的其他引用类型来实现。
  • 从具体类型出发、提取其共性而得出的每一种分组方式都可以表示为一种接口类型。