参考资料:The Go Programming Language

搭建环境

安装

快速切换版本

# 将旧版本备份
cd /usr/local
mv go go.1.10.3

# 将新版本go压缩包解开当前目录
tar zxf go1.16.5.linux-amd64.tar.gz

# 目录下会释放出go目录
ls -l go

go version

前言

简洁的设计需要在工作开始的时候舍弃不必要的想法,并且在软件的生命周期内严格区别好的改变和坏的改变。通过足够的努力,一个好的改变可以在不破坏原有完整概念的前提下保持自适应,正如 Fred Brooks 所说的“概念完整性”;而一个坏的改变则不能达到这个效果,它们仅仅是通过肤浅的和简单的妥协来破坏原有设计的一致性。只有通过简洁的设计,才能让一个系统保持稳定、安全和持续的进化。

Go 语言在设计上的优点:

  • 自动垃圾回收
  • 包系统
  • 函数是一等公民
  • 词法作用域
  • 系统调用接口
  • UTF-8 字符串只读
  • 承诺向后兼容
  • 没有动态语言那样的无类型【id】

Code style

s := ""
var s string
var s = ""
var s string = ""
  • 第一种形式,是一条短变量声明,最简洁,但只能用在函数内部,而不能用于包变量。
  • 第二种形式依赖于字符串的默认初始化零值机制,被初始化为”“。
  • 第三种形式用得很少,除非同时声明多个变量。
  • 第四种形式显式地标明变量的类型,当变量类型与初值类型相同时,类型冗余,但如果两者类型不同,变量类型就必须了。
  • 实践中一般使用前两种形式中的某个,初始值重要的话就显式地指定变量的类型,否则使用隐式初始化。

Import vs. Package

  • 标准库:https://pkg.go.dev/google.golang.org/api/docs/v1

入门章节

GIF 动画

  • 调色板图像:Paletted Image
    • 根据传入的颜色生成一个调色板图像
var palette = []color.Color{color.White, color.Black}
rect := image.Rect(0, 0, 2*size+1, 2*size+1)
img := image.NewPaletted(rect, palette)

获取 URL

  • 强大的 package:net

    • 简单的网络收发信息
    • Go 提供原生的并发特性
  • net/http package

    • Get 返回 resp 结构体,其中的 Body 包括一个可读的服务器响应流
    • resp.Body.Close 关闭,避免数据泄露
    • resp.Status 获取状态码
    • https://pkg.go.dev/net/http@go1.17.2#Response

并发获取多个 URL

  • Fetchall 示例代码

    • 获取所有的 URL,所以这个程序的总执行时间不会超过执行时间最长的那一个任务,前面的 fetch 程序执行时间则是所有任务执行时间之和。
  • goroutine 是一种函数的并发执行方式,而 channel 是用来在 goroutine 之间进行参数传递。

    • main 函数本身也运行在一个 goroutine 中
    • go function 则表示创建一个新的 goroutine,并在这个新的 goroutine 中执行这个 function。
    • 用 make 函数创建了一个传递 string 类型参数的 channel
func main() {
    start := time.Now()
    ch := make(chan string)
    for _, url := range os.Args[1:] {
        go fetch(url, ch) // start a goroutine
    }
    for range os.Args[1:] {
        fmt.Println(<-ch) // receive from channel ch
    }
    fmt.Printf("%.2fs elapsed\n", time.Since(start).Seconds())
}

func fetch(url string, ch chan<- string) {
    start := time.Now()
    resp, err := http.Get(url)
    if err != nil {
        ch <- fmt.Sprint(err) // send to channel ch
        return
    }
    // 可以把ioutil.Discard变量看作一个垃圾桶,可以向里面写一些不需要的数据
    nbytes, err := io.Copy(ioutil.Discard, resp.Body)
    resp.Body.Close() // don't leak resources
    if err != nil {
        ch <- fmt.Sprintf("while reading %s: %v", url, err)
        return
    }
    secs := time.Since(start).Seconds()
    ch <- fmt.Sprintf("%.2fs  %7d  %s", secs, nbytes, url)
}

Web 服务

  • 很简单地完成一个 Web 服务
    • Net package 提供了非常丰富的功能
// Server1 is a minimal "echo" server.
package main

import (
    "fmt"
    "log"
    "net/http"
    "sync"
)

var mu sync.Mutex // 需要在整个程序执行的时候长持
var count int

func main() {
    // 将所有发送到/路径下的请求和handler函数关联起来
    http.HandleFunc("/", handler) // each request calls handler
    
    // 对请求的次数进行计算;对URL的请求结果会包含各种URL被访问的总次数,
    // 直接对/count这个URL的访问要除外。
    http.HandleFunc("/count", counter)
    
    // 监听localhost:8000;
    log.Fatal(http.ListenAndServe("localhost:8000", nil))
}

// handler echoes the Path component of the request URL r.
func handler(w http.ResponseWriter, r *http.Request) {
    // 加锁,维护全局变量
    mu.Lock()
    count++
    mu.Unlock()
    fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
}

// counter echoes the number of calls so far.
func counter(w http.ResponseWriter, r *http.Request) {
    mu.Lock()
    fmt.Fprintf(w, "Count %d\n", count)
    mu.Unlock()
}

指针

  • Go 语言提供了指针。指针是一种直接存储了变量的内存地址的数据类型。

    • C 语言:指针操作是完全不受约束的
    • C++?/ Python:指针一般被处理为“引用”,除了到处传递这些指针之外,并不能对这些指针做太多事情。
  • Go 语言:指针是可见的内存地址,&操作符可以返回一个变量的内存地址,并且*操作符可以获取指针指向的变量内容,但是在 Go 语言里没有指针运算,也就是不能像 c 语言里可以对指针进行加或减操作。

程序结构

命名与可见性

  • 名字的开头字母的大小写决定了名字在包外的可见性
    • 如果一个名字是大写字母开头的(译注:必须是在函数外部定义的包级名字;包级函数名本身也是包级名字),那么它将是导出的,也就是说可以被外部的包访问,例如 fmt 包的 Printf 函数就是导出的,可以在 fmt 包外部访问。

文件结构

  • package:表示该 src 文件属于哪个包
  • import:表示需要导入的依赖包
  • 包级别的 var、const、func、type

变量

  • 零值初始化机制可以确保每个声明的变量总是有一个良好定义的值,因此在 Go 语言中不存在未初始化的变量。
var s string
fmt.Println(s) // ""
  • 在函数内部,有一种称为简短变量声明语句的形式可用于声明和初始化局部变量。它以“名字 := 表达式”形式声明变量,变量的类型根据表达式来自动推导。
anim := gif.GIF{LoopCount: nframes}
freq := rand.Float64() * 3.0
t := 0.0
  • 请记住“:=”是一个变量声明语句,而“=”是一个变量赋值操作。
    • 将右边各个表达式的值赋值给左边对应位置的各个变量:
i, j = j, i // 交换 i 和 j 的值
  • 简短变量声明左边的变量可能并不是全部都是刚刚声明的。如果有一些已经在相同的词法域声明过了(§2.7),那么简短变量声明语句对这些已经声明过的变量就只有赋值行为了。【但是,简短变量声明语句中必须至少要声明一个新的变量】
  • 简短变量声明语句只有对已经在同级词法域声明过的变量才和赋值操作语句等价,如果变量是在外部词法域声明的,那么简短变量声明语句将会在当前词法域重新声明一个新的变量。
in, err := os.Open(infile)
out, err := os.Create(outfile) // OK

f, err := os.Open(infile)
f, err := os.Create(outfile) // compile error: no new variables
f, err = os.Create(outfile) // OK

指针

  • 一个指针的值是另一个变量的地址。
  • 一个指针对应变量在内存中的存储位置。
  • 并不是每一个值都会有一个内存地址,但是对于每一个变量必然有对应的内存地址。
x := 1
p := &x         // p, of type *int, points to x
fmt.Println(*p) // "1"
*p = 2          // equivalent to x = 2
fmt.Println(x)  // "2"
  • 在 Go 语言中,返回函数中局部变量的地址也是安全的。调用 f 函数时创建局部变量 v,在局部变量地址被返回之后依然有效,因为指针 p 依然引用这个变量。
var p = f()

func f() *int {
    v := 1
    return &v
}

fmt.Println(f() == f()) // "false" 每次调用f函数都将返回不同的结果
  • flag package
    • Package flag implements command-line flag parsing.
    • 有三个属性:第一个是命令行标志参数的名字“n”,然后是该标志参数的默认值(这里是 false),最后是该标志参数对应的描述信息。
var n = flag.Bool("n", false, "omit trailing newline")
var sep = flag.String("s", " ", "separator")
  • sepn 变量分别是指向对应命令行标志参数变量的指针,因此必须用 *sep*n 形式的指针语法间接引用它们。
func main() {
    flag.Parse() // 更新每个标志参数对应变量的值(之前是默认值)
    // 非标志参数的普通命令行参数可以通过调用flag.Args()函数来访问,
    // 返回值对应一个字符串类型的slice。
    fmt.Print(strings.Join(flag.Args(), *sep))
    if !*n {
        fmt.Println()
    }
}

new 函数

  • new(T)将创建一个 T 类型的匿名变量,初始化为 T 类型的零值,然后返回变量地址,返回的指针类型为 *T
p := new(int)   // p, *int 类型, 指向匿名的 int 变量
fmt.Println(*p) // "0"
*p = 2          // 设置 int 匿名变量的值为 2
fmt.Println(*p) // "2"

p := new(int)
q := new(int)
fmt.Println(p == q) // "false"
  • 注意:如果两个类型都是空的,也就是说类型的大小是 0,例如 struct{}[0]int,有可能有相同的地址(依赖具体的语言实现)
  • 请谨慎使用大小为 0 的类型,因为如果类型的大小为 0 的话,可能导致 Go 语言的自动垃圾回收器有不同的行为,具体请查看 runtime.SetFinalizer 函数相关文档。

变量声明周期

  • 包级变量

    • 它们的生命周期和整个程序的运行周期是一致的。
  • 局部变量

    • 每次从创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量的存储空间可能被回收。
    • 因为一个变量的有效周期只取决于是否可达,因此一个循环迭代内部的局部变量的生命周期可能超出其局部作用域。
for t := 0.0; t < cycles*2*math.Pi; t += res {
    x := math.Sin(t)
    y := math.Sin(t*freq + phase)
    img.SetColorIndex(
        size+int(x*size+0.5), size+int(y*size+0.5),
        blackIndex, // 最后插入的逗号不会导致编译错误,这是Go编译器的一个特性
    )               // 小括弧另起一行缩进,和大括弧的风格保存一致
}
  • 堆栈分配局部变量存储空间
    • 编译器会自动选择
    • 逃逸的变量需要额外分配内存,同时对性能的优化可能会产生细微的影响。
    • 如果将指向短生命周期对象的指针保存到具有长生命周期的对象中,特别是保存到全局变量时,会阻止对短生命周期对象的垃圾回收(从而可能影响程序的性能)。
var global *int

func f() {
     // x变量必须在堆上分配,因为它在函数退出后依然可以通过包一级的global变量找到,
     // 虽然它是在函数内部定义的
     // x局部变量从f函数中逃逸
    var x int
    x = 1
    global = &x
}

func g() {
    // 函数返回后,y不可达,可以马上被回收
    // 可以栈上分配存储空间
    // 也可以选择在堆上分配,然后由Go语言的GC回收这个变量的内存空间
    y := new(int)
    *y = 1
}

作用域

  • 声明语句的作用域对应的是一个源代码的文本区域;它是一个编译时的属性。

    • 对全局的源代码来说,存在一个整体的词法块,称为全局词法块
    • 对于内置的类型、函数和常量,比如 int、len 和 true 等是在全局作用域的,因此可以在整个程序中直接使用。
    • 当编译器遇到一个名字引用时,它会对其定义进行查找,查找过程从最内层的词法域向全局的作用域进行。如果查找失败,则报告“未声明的名字”这样的错误。如果该名字在内部和外部的块分别声明过,则内部块的声明首先被找到。
  • 一个变量的生命周期是指程序运行时变量存在的有效时间段,在此时间区域内它可以被程序的其他部分引用;是一个运行时的概念。

Slice 的进阶:[]T 和[]*T 的选型

  • 参考文献:[]T 还是 []*T, 这是一个问题 (colobu.com)

    • 对于 Go 语言,严格意义上来讲,只有一种传递,也就是按值传递(by value)。当一个变量当作参数传递的时候,会创建一个变量的副本,然后传递给函数或者方法,你可以看到这个副本的地址和变量的地址是不一样的。
    • 当变量当做指针被传递的时候,一个新的指针被创建,它指向变量指向的同样的内存地址,所以你可以将这个指针看成原始变量指针的副本。当这样理解的时候,我们就可以理解成 Go 总是创建一个副本按值转递,只不过这个副本有时候是变量的副本,有时候是变量指针的副本。
  • T 作为参数传递

package main
import "fmt"
type Bird struct {
        Age  int
        Name string
}
func passV(b Bird) {
        b.Age++
        b.Name = "Great" + b.Name
        fmt.Printf("传入修改后的Bird:\t %+v, \t内存地址:%p\n", b, &b)
}
func main() {
        parrot := Bird{Age: 1, Name: "Blue"}
        fmt.Printf("原始的Bird:\t\t %+v, \t\t内存地址:%p\n", parrot, &parrot)
        passV(parrot)
        fmt.Printf("调用后原始的Bird:\t %+v, \t\t内存地址:%p\n", parrot, &parrot)
}

// 原始的Bird:                 {Age:1 Name:Blue},                 内存地址:0xc420012260
// 传入修改后的Bird:         {Age:2 Name:GreatBlue},         内存地址:0xc4200122c0
// 调用后原始的Bird:         {Age:1 Name:Blue},                 内存地址:0xc420012260
  • *T 作为参数传递 => 调用的时候需要传入一个实例的指针 => 取得实例指针的方法 &< 实例 >
package main
import "fmt"
type Bird struct {
        Age  int
        Name string
}
func passP(b *Bird) {
        b.Age++
        b.Name = "Great" + b.Name
        fmt.Printf("传入修改后的Bird:\t %+v, \t内存地址:%p, 指针的内存地址: %p\n", *b, b, &b)
}
func main() {
        parrot := &Bird{Age: 1, Name: "Blue"}
        fmt.Printf("原始的Bird:\t\t %+v, \t\t内存地址:%p, 指针的内存地址: %p\n", *parrot, parrot, &parrot)
        passP(parrot)
        fmt.Printf("调用后原始的Bird:\t %+v, \t内存地址:%p, 指针的内存地址: %p\n", *parrot, parrot, &parrot)
}

// 原始的Bird:                 {Age:1 Name:Blue},                 内存地址:0xc420076000, 指针的内存地址: 0xc420074000
// 传入修改后的Bird:         {Age:2 Name:GreatBlue},         内存地址:0xc420076000, 指针的内存地址: 0xc420074010
// 调用后原始的Bird:         {Age:2 Name:GreatBlue},         内存地址:0xc420076000, 指针的内存地址: 0xc420074000
  • T 和*T 的选型

    1. 不想变量被修改。 如果你不想变量被函数和方法所修改,那么选择类型 T。相反,如果想修改原始的变量,则选择 *T
    2. 如果变量是一个的 struct 或者数组,则副本的创建相对会影响性能,这个时候考虑使用 *T,只创建新的指针,这个区别是巨大的
    3. (不针对函数参数,只针对本地变量/本地变量)对于函数作用域内的参数,如果定义成 T,Go 编译器尽量将对象分配到栈上,而 *T 很可能会分配到对象上,这对垃圾回收会有影响
  • 什么时候会发生拷贝

    • 赋值发生拷贝
package main
import "fmt"
type Bird struct {
        Age  int
        Name string
}
type Parrot struct {
        Age  int
        Name string
}
var parrot1 = Bird{Age: 1, Name: "Blue"}
var parrot2 = parrot1
func main() {
        fmt.Printf("parrot1:\t\t %+v, \t\t内存地址:%p\n", parrot1, &parrot1)
        fmt.Printf("parrot2:\t\t %+v, \t\t内存地址:%p\n", parrot2, &parrot2)
        parrot3 := parrot1
        fmt.Printf("parrot3:\t\t %+v, \t\t内存地址:%p\n", parrot3, &parrot3)
        parrot4 := Parrot(parrot1)
        fmt.Printf("parrot4:\t\t %+v, \t\t内存地址:%p\n", parrot4, &parrot4)
}

// parrot1:                 {Age:1 Name:Blue},                 内存地址:0xfa0a0
// parrot2:                 {Age:1 Name:Blue},                 内存地址:0xfa0c0
// parrot3:                 {Age:1 Name:Blue},                 内存地址:0xc42007e0c0
// parrot4:                 {Age:1 Name:Blue},                 内存地址:0xc42007e100
  • slice,map 和数组在初始化和按索引设置的时候也会创建副本
package main
import "fmt"
type Bird struct {
        Age  int
        Name string
}
var parrot1 = Bird{Age: 1, Name: "Blue"}
func main() {
        fmt.Printf("parrot1:\t\t %+v, \t\t内存地址:%p\n", parrot1, &parrot1)
        //slice
        s := []Bird{parrot1}
        s = append(s, parrot1)
        parrot1.Age = 3
        fmt.Printf("parrot2:\t\t %+v, \t\t内存地址:%p\n", s[0], &(s[0]))
        fmt.Printf("parrot3:\t\t %+v, \t\t内存地址:%p\n", s[1], &(s[1]))
        parrot1.Age = 1
        //map
        m := make(map[int]Bird)
        m[0] = parrot1
        parrot1.Age = 4
        fmt.Printf("parrot4:\t\t %+v\n", m[0])
        parrot1.Age = 5
        parrot5 := m[0]
        fmt.Printf("parrot5:\t\t %+v, \t\t内存地址:%p\n", parrot5, &parrot5)
        parrot1.Age = 1
        //array
        a := [2]Bird{parrot1}
        parrot1.Age = 6
        fmt.Printf("parrot6:\t\t %+v, \t\t内存地址:%p\n", a[0], &a[0])
        parrot1.Age = 1
        a[1] = parrot1
        parrot1.Age = 7
        fmt.Printf("parrot7:\t\t %+v, \t\t内存地址:%p\n", a[1], &a[1])
}

// parrot1:                 {Age:1 Name:Blue},                 内存地址:0xfa0a0
// parrot2:                 {Age:1 Name:Blue},                 内存地址:0xc4200160f0
// parrot3:                 {Age:1 Name:Blue},                 内存地址:0xc420016108
// parrot4:                 {Age:1 Name:Blue}
// parrot5:                 {Age:1 Name:Blue},                 内存地址:0xc420012320
// parrot6:                 {Age:1 Name:Blue},                 内存地址:0xc420016120
// parrot7:                 {Age:1 Name:Blue},                 内存地址:0xc420016138
  • for-range 循环也是将元素的副本赋值给循环变量,所以变量得到的是集合元素的副本。
package main
import "fmt"
type Bird struct {
        Age  int
        Name string
}
var parrot1 = Bird{Age: 1, Name: "Blue"}
func main() {
        fmt.Printf("parrot1:\t\t %+v, \t\t内存地址:%p\n", parrot1, &parrot1)
        //slice
        s := []Bird{parrot1, parrot1, parrot1}
        s[0].Age = 1
        s[1].Age = 2
        s[2].Age = 3
        parrot1.Age = 4
        for i, p := range s {
                fmt.Printf("parrot%d:\t\t %+v, \t\t内存地址:%p\n", (i + 2), p, &p)
        }
        parrot1.Age = 1
        //map
        m := make(map[int]Bird)
        parrot1.Age = 1
        m[0] = parrot1
        parrot1.Age = 2
        m[1] = parrot1
        parrot1.Age = 3
        m[2] = parrot1
        parrot1.Age = 4
        for k, v := range m {
                fmt.Printf("parrot%d:\t\t %+v, \t\t内存地址:%p\n", (k + 2), v, &v)
        }
        parrot1.Age = 4
        //array
        a := [...]Bird{parrot1, parrot1, parrot1}
        a[0].Age = 1
        a[1].Age = 2
        a[2].Age = 3
        parrot1.Age = 4
        for i, p := range a {
                fmt.Printf("parrot%d:\t\t %+v, \t\t内存地址:%p\n", (i + 2), p, &p)
        }
}

parrot1:                 {Age:1 Name:Blue},                 内存地址:0xfb0a0
parrot2:                 {Age:1 Name:Blue},                 内存地址:0xc4200122a0
parrot3:                 {Age:2 Name:Blue},                 内存地址:0xc4200122a0
parrot4:                 {Age:3 Name:Blue},                 内存地址:0xc4200122a0
parrot2:                 {Age:1 Name:Blue},                 内存地址:0xc420012320
parrot3:                 {Age:2 Name:Blue},                 内存地址:0xc420012320
parrot4:                 {Age:3 Name:Blue},                 内存地址:0xc420012320
parrot2:                 {Age:1 Name:Blue},                 内存地址:0xc4200123a0
parrot3:                 {Age:2 Name:Blue},                 内存地址:0xc4200123a0
parrot4:                 {Age:3 Name:Blue},                 内存地址:0xc4200123a0
  • channel 变量
  • 对于 [...]T[...]*T 的区别,我想你也应该清楚了,[...]*T 创建的副本的元素时元数组元素指针的副本。

类型

  • 一个类型声明语句创建了一个新的类型名称,和现有类型具有相同的底层结构。新命名的类型提供了一个方法,用来分隔不同概念的类型,这样即使它们底层类型相同也是不兼容的。

    • type 类型名字 底层类型
  • 类型声明语句一般出现在包一级,因此如果新创建的类型名字的首字符大写,则在包外部也可以使用。

// Package tempconv performs Celsius and Fahrenheit temperature computations.
package tempconv

import "fmt"

type Celsius float64    // 摄氏温度
type Fahrenheit float64 // 华氏温度

const (
    AbsoluteZeroC Celsius = -273.15 // 绝对零度
    FreezingC     Celsius = 0       // 结冰点温度
    BoilingC      Celsius = 100     // 沸水温度
)

func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }
func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }

var c Celsius
var f Fahrenheit
fmt.Println(c == 0)          // "true"
fmt.Println(f >= 0)          // "true"
fmt.Println(c == f)          // 不能直接比较类型 compile error: type mismatch
fmt.Println(c == Celsius(f)) // 支持类型转换 "true"!
  • 类型方法
func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }

fmt.Println(c.String()) // "100°C"

c := FToC(212.0)
fmt.Println(c.String()) // "100°C"
fmt.Printf("%v\n", c)   // "100°C"; no need to call String explicitly
fmt.Printf("%s\n", c)   // "100°C"
fmt.Println(c)          // "100°C"
fmt.Printf("%g\n", c)   // "100"; does not call String, 打印浮点数,将采用更紧凑的表示形式打印,并提供足够的精度
fmt.Println(float64(c)) // "100"; does not call String

包和文件

  • Go 语言中的包和其他语言的库或模块的概念类似,目的都是为了支持模块化、封装、单独编译和代码重用。
  • 除了包的导入路径,每个包还有一个包名,包名一般是短小的名字(并不要求包名是唯一的),包名在包的声明处指定。
  • 如果导入了一个包,但是又没有使用该包将被当作一个编译错误处理。

    • 可以使用 golang.org/x/tools/cmd/goimports 导入工具,它可以根据需要自动添加或删除导入的包

包的初始化

func init() { /* ... */ }
  • 这样的 init 初始化函数除了不能被调用或引用外,其他行为和普通函数类似。
  • 在每个文件中的 init 初始化函数,在程序开始执行时按照它们声明的顺序被自动调用。
  • 初始化工作是自下而上进行的,main 包最后被初始化。以这种方式,可以确保在 main 函数执行之前,所有依赖的包都已经完成初始化工作了。

基础数据类型

整型

  • Printf 格式化字符串包含多个 % 参数时将会包含对应相同数量的额外操作数,但是 % 之后的 [1] 副词告诉 Printf 函数再次使用第一个操作数。
  • % 后的 # 副词告诉 Printf 在用 %o、%x 或 %X 输出时生成 0、0x 或 0X 前缀。
o := 0666
fmt.Printf("%d %[1]o %#[1]o\n", o) // "438 666 0666"
x := int64(0xdeadbeef)
fmt.Printf("%d %[1]x %#[1]x %#[1]X\n", x)
// Output:
// 3735928559 deadbeef 0xdeadbeef 0XDEADBEEF

浮点数

  • 测试一个结果是否是非数 NaN 则是充满风险的,因为 NaN 和任何数都是不相等的(译注:在浮点数中,NaN、正无穷大和负无穷大都不是唯一的,每个都有非常多种的 bit 模式表示)
nan := math.NaN()
fmt.Println(nan == nan, nan < nan, nan > nan) // "false false false"

字符串

  • RuneCountInString
import "unicode/utf8"

s := "Hello, 世界"
fmt.Println(len(s))                    // "13"
fmt.Println(utf8.RuneCountInString(s)) // "9"
  • UTF8 解码器 in unicode/utf8
    • 如果遇到一个错误的 UTF8 编码输入,将生成一个特别的 Unicode 字符 \uFFFD,在印刷中这个符号通常是一个黑色六角或钻石形状,里面包含一个白色的问号”?”。
for i := 0; i < len(s); {
    r, size := utf8.DecodeRuneInString(s[i:])
    fmt.Printf("%d\t%c\n", i, r)
    i += size
}
  • []rune 类型转换应用到 UTF8 编码的字符串,将返回字符串编码的 Unicode 码点序列
// "program" in Japanese katakana
s := "プログラム"
fmt.Printf("% x\n", s) // "e3 83 97 e3 83 ad e3 82 b0 e3 83 a9 e3 83 a0"
r := []rune(s)
fmt.Printf("%x\n", r)  // "[30d7 30ed 30b0 30e9 30e0]"

fmt.Println(string(r)) // "プログラム"

Byte 切片

  • []byte【和字符串有着相同结构的[]byte 类型】: 字符串是只读的,因此逐步构建字符串会导致很多分配和复制。在这种情况下,使用 bytes.Buffer 类型将会更有效。
  • strconv: 布尔型、整型数、浮点数和对应字符串的相互转换
  • unicode: IsDigit、IsLetter、IsUpper、IsLower

常量

  • 常量表达式的值在编译期计算,而不是在运行期。
  • 常量声明可以使用 iota 常量生成器初始化,它用于生成一组以相似规则初始化的常量,但是不用每行都写一遍初始化表达式。

    • 周日将对应 0,周一为 1
type Weekday int

const (
    Sunday Weekday = iota
    Monday
    Tuesday
    Wednesday
    Thursday
    Friday
    Saturday
)
  • net 库中的 Flag
type Flags uint
const (
    FlagUp           Flags = 1 << iota // 接口在活动状态
    FlagBroadcast                      // 接口支持广播
    FlagLoopback                       // 接口是环回的
    FlagPointToPoint                   // 接口是点对点的
    FlagMulticast                      // 接口支持组播
)

复合数据类型

数组[一般指的是长度固定的 Slice?反正很少用]

  • 数组的长度是固定的,Go 语言中很少使用数组,而是使用 Slice,它是可以增长和收缩的动态序列。
  • 数组依然很少用作函数参数;相反,我们一般使用 slice 来替代数组。
q := [...]int{1, 2, 3} // ...表示根据数组长度计算个数
fmt.Printf("%T\n", q) // "[3]int"
  • 数组的比较
a := [2]int{1, 2}
b := [...]int{1, 2}
c := [2]int{1, 3}
fmt.Println(a == b, a == c, b == c) // "true false false"
d := [3]int{1, 2}
fmt.Println(a == d) // compile error: cannot compare [2]int == [3]int
  • crypto/sha256 包 Sum256 函数对一个任意的字节 slice 类型的数据生成对应的消息摘要
    • 消息摘要有 256bit 大小,因此对应[32]byte 数组类型。
import "crypto/sha256"

func main() {
    c1 := sha256.Sum256([]byte("x"))
    c2 := sha256.Sum256([]byte("X"))
    fmt.Printf("%x\n%x\n%t\n%T\n", c1, c2, c1 == c2, c1)
    // Output:
    // 2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881
    // 4b68ab3847feda7d6c62c1fbcbeebfa35eab7351ed5e78f4ddadea5df64b8015
    // false
    // [32]uint8 - %T打印类型
}

Slice

  • Slice 是变长的序列,序列中每个元素有相同的类型。 写作[]T

    • 一个 slice 由三个部分构成:指针、长度和容量。
    • 指针指向第一个 slice 元素对应的底层数组元素的地址,slice 的第一个元素并不一定就是数组的第一个元素
    • 长度不能超过容量【len】
    • 容量一般是从 slice 的开始位置到底层数据的结尾位置【cap】
  • slice 的切片操作 s[i:j],其中 0 ≤ i≤ j≤ cap(s),用于创建一个新的 slice,引用 s 的从第 i 个元素开始到第 j-1 个元素的子序列。

    • 如果切片操作超出 cap(s)的上限将导致一个 panic 异常,但是超出 len(s)则是意味着扩展了 slice,因为新 slice 的长度会变大
months := [...]string{1: "January", /* ... */, 12: "December"}
summer := months[6:9]
fmt.Println(summer) // ["June" "July" "August"]
fmt.Println(summer[:20]) // panic: out of range
endlessSummer := summer[:5] // extend a slice (within capacity)
fmt.Println(endlessSummer)  // "[June July August September October]"
  • Slice 不直接支持比较运算
    • Slice 的元素是间接引用的,甚至可以包含自身
    • 一个固定的 Slice 值在不同的时刻可能包含不同的元素,因为底层数组的元素可能被修改。
    • 测试一个 slice 是否为空的方法:len(s) == 0, 而不能用 s == nil
var s []int    // len(s) == 0, s == nil
s = nil        // len(s) == 0, s == nil
s = []int(nil) // len(s) == 0, s == nil
s = []int{}    // len(s) == 0, s != nil

append 操作

// 根据cap和len去扩展Slide的容量
func appendInt(x []int, y int) []int {
    var z []int
    zlen := len(x) + 1
    if zlen <= cap(x) {
        // There is room to grow.  Extend the slice.
        z = x[:zlen]
    } else {
        // There is insufficient space.  Allocate a new array.
        // Grow by doubling, for amortized linear complexity.
        zcap := zlen
        if zcap < 2*len(x) {
            zcap = 2 * len(x)
        }
        z = make([]int, zlen, zcap)
        // z: target; x: src
        copy(z, x) // a built-in function; see text
    }
    z[len(x)] = y
    return z
}

Slice 内存技巧

  • 模拟栈
// push
stack = append(stack, v)
// top
top := stack[len(stack) - 1]
// pop
stack = stack[:len(stack) - 1]
// remove i
func remove(slice []int, i int) {
    copy(slice[i:], slice[i+1:])
    return slice[:len(slice) - 1]
}

Slice 排序 - 两种方法

  • 方案 1:使用 sort 包中原生的 Slice 和 SliceStable 进行排序,比较简单
    • sort.SliceStable(arr, func(i int, j int) bool {}):排序的时候会保留相同值元素的原始顺序
    • sort.Slice(arr, func(i int, j int) bool {})
len := len(names)
arr := make([]NameItem, len)

for i := 0; i < len; i++ {
    arr[i] = NameItem{name: names[i], height: heights[i]}
}

sort.SliceStable(arr, func(i int, j int) bool {
    return arr[i].height > arr[j].height
})
  • 方案 2:先声明一个 interface(即可以存函数方法签名的方案),然后声明一个 slice 的结构体,实现 interface 的方法,最后使用 sort.Sort 和 sort.Stable 可以进行排序
    • sort.Sort()
    • sort.Stable()
type ArrSortInterface interface {
    Len() int
    Swap(i int, j int)
    Less(i int, j int) bool
}

type NameItemArr []NameItem

func (arr NameItemArr) Len() int {
    return len(arr)
}

func (arr NameItemArr) Swap(i int, j int) {
    arr[i], arr[j] = arr[j], arr[i]
}

func (arr NameItemArr) Less(i int, j int) bool {
    return arr[i].height < arr[j].height
}

sort.Sort(NameItemArr(arr))

二维 Slice 的构建

n := len(matrix)
dp := make([][]int, n)
for i := 0; i < len(matrix); i++ {
    dp[i] = make([]int, n)
}

Map

  • 初始构建
    • 安全的增加、删除和查询
ages := map[string]int{
    "alice":   31,
    "charlie": 34,
}

delete(ages, "alice") // remove element ages["alice"]
  • 禁止对 map 元素取址
    • map 可能随着元素数量的增长而重新分配更大的内存空间,从而可能导致之前的地址无效。
_ = &ages["bob"] // compile error: cannot take address of map element

Map 的声明必须带着初始化

func distinctAverages(nums []int) int {
    var result_avg map[float32]int = make(map[float32]int) // 必须加上make, 不然会报错
    return len(result_avg)
}

Map 排序

  • 显式地对 key 进行排序,可以使用 sort 包的 Strings 函数对字符串 slice 进行排序。
import "sort"

var names []string
for name := range ages {
    names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
    fmt.Printf("%s\t%d\n", name, ages[name])
}
  • 判断某个 key 是否在 map 中存在
    • map 的下标语法将产生两个值;第二个是一个布尔值,用于报告元素是否真的存在。布尔变量一般命名为 ok,特别适合马上用于 if 条件判断部分。
if age, ok := ages["bob"]; !ok { /* ... */ }

结构体

  • 因为在 Go 语言中,所有的函数参数都是值拷贝传入的,函数参数将不再是函数调用时的原始变量。如果要在函数内部修改结构体成员的话,用指针传入是必须的
func AwardAnnualRaise(e *Employee) {
    e.Salary = e.Salary * 105 / 100
}

结构体嵌入和匿名成员

  • Go 语言有一个特性让我们只声明一个成员对应的数据类型而不指名成员的名字;这类成员就叫匿名成员。
  • 匿名成员的数据类型必须是命名的类型或指向一个命名的类型的指针。
  • 匿名嵌入的特性,我们可以直接访问叶子属性而不需要给出完整的路径
type Circle struct {
    Point
    Radius int
}

type Wheel struct {
    Circle
    Spokes int
}

var w Wheel
w.X = 8            // equivalent to w.Circle.Point.X = 8
w.Y = 8            // equivalent to w.Circle.Point.Y = 8
w.Radius = 5       // equivalent to w.Circle.Radius = 5
w.Spokes = 20

JSON

  • 将一个 Go 语言中类似 movies 的结构体 slice 转为 JSON 的过程叫编组(marshaling)。
data, err := json.Marshal(movies)
  • 编组的时候增加格式
data, err := json.MarshalIndent(movies, "", "    ")
// 每一行输出的前缀: ""
// 每一个层级的缩进: "    "
  • 结构体成员 Tag:和在编译阶段关联到该成员的元信息字符串
    • omitempty 选项,表示当 Go 语言结构体成员为空或零值时不生成该 JSON 对象(这里 false 为零值)。
type Movie struct {
    Title  string
    Year   int  `json:"released"`
    Color  bool `json:"color,omitempty"`
    Actors []string
}
  • 编码的逆操作是解码,Go 语言中一般叫 unmarshaling,通过 json.Unmarshal 函数完成。
[
    {
        "Title": "Casablanca",
        "released": 1942,
        "Actors": [
            "Humphrey Bogart",
            "Ingrid Bergman"
        ]
    },
    {
        "Title": "Cool Hand Luke",
        "released": 1967,
        "color": true,
        "Actors": [
            "Paul Newman"
        ]
    },
    {
        "Title": "Bullitt",
        "released": 1968,
        "color": true,
        "Actors": [
            "Steve McQueen",
            "Jacqueline Bisset"
        ]
    }
]

var titles []struct{ Title string }
if err := json.Unmarshal(data, &titles); err != nil {
    log.Fatalf("JSON unmarshaling failed: %s", err)
}
fmt.Println(titles) // "[{Casablanca} {Cool Hand Luke} {Bullitt}]"
  • 基于流式的解码器 json.Decoder,可以从一个输入流解码 JSON 数据,还有一个针对输出流的 json.Encoder 编码对象。
  • 几个练习很有意思!!!

文本和 HTML 模板

  • 本节主要是在生成 HTML 相关的方法,很有用。

函数

多返回值

  • 准确的变量名可以传达函数返回值的含义。尤其在返回值的类型都相同时,就像下面这样:
func Size(rect image.Rectangle) (width, height int)
func Split(path string) (dir, file string)
func HourMinSec(t time.Time) (hour, minute, second int)
  • bare return: 如果一个函数所有的返回值都有显式的变量名,那么该函数的 return 语句可以省略操作数。
// return 等价于
// return words, images, err
func CountWordsAndImages(url string) (words, images int, err error) {
    resp, err := http.Get(url)
    if err != nil {
        return
    }
    doc, err := html.Parse(resp.Body)
    resp.Body.Close()
    if err != nil {
        err = fmt.Errorf("parsing HTML: %s", err)
        return
    }
    words, images = countWordsAndImages(doc)
    return
}
func countWordsAndImages(n *html.Node) (words, images int) { /* ... */ }

匿名函数

  • 只能在包级语法块中被声明。
  • 闭包

    • 在 squares 中定义的匿名内部函数可以访问和更新 squares 中的局部变量,这意味着匿名函数和 squares 中,存在变量引用。
    • 在构建递归闭包的时候,需要先声明 block,再给 block 赋值,不然会找不到这个 block
// squares返回一个匿名函数。
// 该匿名函数每次被调用时都会返回下一个数的平方。
func squares() func() int {
    var x int
    return func() int {
        x++
        return x * x
    }
}
func main() {
    f := squares()
    fmt.Println(f()) // "1"
    fmt.Println(f()) // "4"
    fmt.Println(f()) // "9"
    fmt.Println(f()) // "16"
}
func topoSort(m map[string][]string) []string {
    var order []string
    seen := make(map[string]bool)
    var visitAll func(items []string)
    visitAll = func(items []string) {
        for _, item := range items {
            if !seen[item] {
                seen[item] = true
                visitAll(m[item])
                order = append(order, item)
            }
        }
    }
    var keys []string
    for key := range m {
        keys = append(keys, key)
    }
    sort.Strings(keys)
    visitAll(keys)
    return order
}
  • 闭包的作用域问题
    • 类似 Swift 中的异步函数
    • for 循环语句引入了新的词法块,循环变量 dir 在这个词法块中被声明。在该循环中生成的所有函数值都共享相同的循环变量。
    • 函数值中记录的是循环变量的内存地址,而不是循环变量某一时刻的值。
    • 当删除操作执行时,for 循环已完成,dir 中存储的值等于最后一次迭代的值。
var rmdirs []func()
for _, dir := range tempDirs() {
    os.MkdirAll(dir, 0755)
    rmdirs = append(rmdirs, func() {
        os.RemoveAll(dir) // NOTE: incorrect!
    })
}

可变参数

  • 在声明可变参数函数时,需要在参数列表的最后一个参数类型之前加上省略符号“…”,这表示该函数会接收任意数量的该类型参数。
    • 内部机制:vals 被看作是类型为[] int 的切片。
    • 如果原始参数已经是切片类型:需在最后一个参数后加上省略符。
func sum(vals ...int) int {
    total := 0
    for _, val := range vals {
        total += val
    }
    return total
}

values := []int{1, 2, 3, 4}
fmt.Println(sum(values...)) // "10"

Defered 函数

  • 循环体中的 defer 语句:
    • 只有在函数执行完毕后,这些被延迟的函数才会执行。下面的代码会导致系统的文件描述符耗尽,因为在所有文件都被处理之前,没有文件会被关闭。
for _, filename := range filenames {
    f, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer f.Close() // NOTE: risky; could run out of file descriptors
    // ...process f…
}
  • 改进方法:抽象一个函数出来
for _, filename := range filenames {
    if err := doFile(filename); err != nil {
        return err
    }
}
func doFile(filename string) error {
    f, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer f.Close()
    // ...process f…
}
  • 关闭文件读写的时候采用 defer 机制
    • 许多文件系统,尤其是 NFS,写入文件时发生的错误会被延迟到文件关闭时反馈。如果没有检查文件关闭时的反馈信息,可能会导致数据丢失,而我们还误以为写入操作成功。

Panic 异常

  • 发生时机:

    • 运行时才会检查的错误,如数组访问越界、空指针应用,这些错误会引起 painc 异常
    • 内置的 panic 函数,可以接受任何值作为参数【一般用于严重错误,避免程序崩溃】
  • painc 发生后:

    • painc 异常发生后,会立即执行 goroutine 中被延迟的函数(defer 机制),程序崩溃并输出日志。
    • 在 Go 的 panic 机制中,延迟函数的调用在释放堆栈信息之前,所以 runtime.Stack 可以输出被释放函数的信息
func main() {
    defer printStack()
    f(3)
}
func f(x int) {
    fmt.Printf("f(%d)\n", x+0/x) // panics if x == 0
    defer fmt.Printf("defer %d\n", x)
    f(x - 1)
}

/*
goroutine 1 [running]:
main.printStack()
src/gopl.io/ch5/defer2/defer.go:20
main.f(0)
src/gopl.io/ch5/defer2/defer.go:27
main.f(1)
src/gopl.io/ch5/defer2/defer.go:29
main.f(2)
src/gopl.io/ch5/defer2/defer.go:29
main.f(3)
src/gopl.io/ch5/defer2/defer.go:29
main.main()
src/gopl.io/ch5/defer2/defer.go:15
*/

Recover 捕获异常

  • panic 异常后需要从异常中恢复,在崩溃前的操作情况:
    • 在 defer 函数中调用内置函数 recover,则程序会从 panic 中恢复,并返回 panic value
func Parse(input string) (s *Syntax, err error) {
    defer func() {
        if p := recover(); p != nil {
            err = fmt.Errorf("internal error: %v", p)
        }
    }()
    // ...parser...
}
  • panic 的处理需要的区分各种状态
    • 公有的 API 需要将函数的运行失败作为 error 返回,而不是 panic
    • 不应该恢复由他人开发的函数引起的 panic
    • net/http 包 的 web 请求失败的 panic 异常不能直接杀掉整个进程,而且 web 服务器遇到 panic 调用 recover 会引起内存泄漏

函数修改外部变量的内容

  • 参考文档:Golang 是值传递还是引用传递 - 知乎 (zhihu.com)
  • Go 不存在引用传递, 传进来的都是值,
  • int, string 这种值类型变量, 就是一次深拷贝

    • 如果想要在调用函数时, 修改传入参数的值, 需要传进来指针类型(current string), 这时候函数会生成一份指针的拷贝, 该指针的拷贝和原指针一样,指向同一个内存中实例的地址。如果想要修改, 则需要进行解引用(current_str), 这样就能获得那个地址的值了, 这时可以获取或者赋值等操作
  • 对于引用类型变量(slice, map, channel, interface, func)等, 其实他们在构造的时候返回的其实是一个指针

    • map == *hamp,故如果写了一个函数为 func modify(m map)等价于 func modify(m *hmap),然后在 modify 函数内部使用 map 类型进行操作, 本质上也是解引用后对内存中的值进行直接操作, 因此这样就可以解决了!
    • slice, 这个和 map 还不太一样. 底层中 slide = type slide struct {array Pointer, len int, cap int},所以如果只传 slide 类型, 那么其实还是做了一个值拷贝, 其带的 Pointer 会发生改变, 但还是指向同一个空间, 但是如果当扩容的时候有可能导致空间的首地址发生变化。正确的方式是传([]int), 即传一个 slice, 这样就可以了
func backward(n int, res *[]string, current_str *string, left_num int, right_num int) {
    if right_num > left_num {
        return
    }
    if right_num > n || left_num > n {
        return
    }
    if right_num == n && left_num == n {
        *res = append(*res, *current_str)
        return
    }

    *current_str = *current_str + "("
    backward(n, res, current_str, left_num+1, right_num)
    *current_str = (*current_str)[:len(*current_str)-1]

    *current_str = *current_str + ")"
    backward(n, res, current_str, left_num, right_num+1)
    *current_str = (*current_str)[:len(*current_str)-1]
}

func generateParenthesis(n int) []string {
    res := []string{}
    current_str := ""
    backward(n, &res, &current_str, 0, 0)
    return res
}

func main() {
    n := 3
    result := generateParenthesis(n)
    fmt.Println("result: ")
    for _, s := range result {
        fmt.Println(s)
    }
}

特殊函数

Min/Max

  • 参考文献:go 语言为什么没有 min/max(int, int)函数 - 简书 (jianshu.com)
  • go 语言 math 包里面定义了 min/max 函数,但是是 float64 类型的,而并没有整数类型的 min/max。由于 float64 类型要处理 infinity 和 not-a-number 这种值,而他们的处理非常复杂,一般用户没有能力,所有 go 需要为用户提供系统级别的解决办法。对于 int/int64 类型的数据,min/max 的实现非常简单直接,用户完全可以自己实现
func Min(x int, y int) int {
    if x < y {
        return x
    }
    return y
}
  • 推广到多个参数的情况,可以使用可变参数!
func Min(first int, others ...int) int {
    res := first
    for _, o := range others {
        // fmt.Println("o: ", o)
        if o < res {
            res = o
        }
    }
    // fmt.Println("min left right midium: ", res)
    return res
}

包和工具

本节其实在入门的时候 会更加重要,不知道为什么本书的作者把它放在了后面。

工具

【本节内容在我构建 Go 环境的时候经常会遇到一些神奇的问题,因此专门拿出来总结】

  • 类似于 Cargo、npm、pod、bundle 这样的包管理器
  • 一个单元测试和基准测试的驱动程序
  • Go build

    • 默认情况下,go build 命令构建指定的包和它依赖的包,然后丢弃除了最后的可执行文件之外所有的中间编译结果。依赖分析和编译过程虽然都是很快的,但是随着项目增加到几十个包和成千上万行代码,依赖关系分析和编译时间的消耗将变的可观,有时候可能需要几秒种,即使这些依赖项没有改变。
    • 不会重新编译没有发生变化的包,这可以使后续构建更快捷。
  • Go install

    • 它会保存每个包的编译成果,而不是将它们都丢弃。被编译的包会被保存到$GOPATH/pkg目录下,目录路径和 src目录路径对应,可执行程序被保存到$GOPATH/bin 目录。
    • 不会重新编译没有发生变化的包,这可以使后续构建更快捷。
  • 根据平台选择构建包
    • go build只在编译程序对应的目标操作系统是Linux或Mac OS X时才编译这个文件。

包文档

好的文档并不需要面面俱到,文档本身应该是简洁但不可忽略的。事实上,Go 语言的风格更喜欢简洁的文档,并且文档也是需要像代码一样维护的。对于一组声明语句,可以用一个精炼的句子描述,如果是显而易见的功能则并不需要注释。

go doc time

在线服务文档:https://godoc.org/,包含了成千上万的开源包的检索工具。

内部包:internal 包

  • 使用场景

    • 我们计划将一个大的包拆分为很多小的更容易维护的子包,但是我们并不想将内部的子包结构也完全暴露出去。
    • 希望在内部子包之间共享一些通用的处理包,或者我们只是想实验一个新包的还并不稳定的接口,暂时只暴露给一些受限制的用户使用。
  • 解决方案

    • 一个 internal 包只能被和 internal 目录有同一个父目录的包所导入。
net/http/internal/chunked [internal]

net/http [ok]
net/http/httputil [ok]
net/url [fail]

查询包

go list github.com/go-sql-driver/mysql
go list -json hash // 查看metadata

vendor 文件夹进行包管理

Golang 反射

  • 提前阅读该章节的目的是:为学习 YoMo Codec 打基础。
  • 反射的机制作用:能够在运行时更新变量和检查它们的值、调用它们的方法和它们支持的内在操作,而不需要在编译时就知道这些变量的具体类型。
  • 没有办法来检查未知类型的表示方式,我们被卡住了。这就是我们为何需要反射的原因。

reflect.Type 和 reflect.Value

  • reflect.Type: Go 类型,是一个接口,有许多方法来区分类型以及检查组成部分。
  • reflect.TypeOf: 接受 interface{}类型,返回 relect.Type 的动态类型

    • fmt.Printf 提供了 %T 参数,内部就是用 reflect.TypeOf 输出
t := reflect.TypeOf(3)  // a reflect.Type
fmt.Println(t.String()) // "int"
fmt.Println(t)          // "int"

fmt.Printf("%T\n", 3) // "int"
  • reflect.Value:可以装载任何类型的值

    • 包含 Kind 方法,可以用 Value 去判断底层的类型,包含的类型有限:Bool、String 和 所有数字类型的基础类型;Array 和 Struct 对应的聚合类型;Chan、Func、Ptr、Slice 和 Map 对应的引用类型;interface 类型;表示空值的 Invalid 类型。
  • reflect.ValueOf:接受 interface{}类型,返回一个装载动态值的 reflect.Value

    • 逆操作:reflect.Value.Interface
v := reflect.ValueOf(3) // a reflect.Value
fmt.Println(v)          // "3"
fmt.Printf("%v\n", v)   // "3"
fmt.Println(v.String()) // NOTE: "<int Value>"

t := v.Type()           // a reflect.Type
fmt.Println(t.String()) // "int"

x := v.Interface()      // an interface{}
i := x.(int)            // an int
fmt.Printf("%d\n", i)   // "3"

Display 递归值打印器

  • 使用反射的特性可以实现使用 Go 语言将一个未知格式的输入解析打印出来

    • 递归的意思是:如果遇到了数组、结构体、map,会使用递归的方法去获取内容
  • 问题:如果遇到对象图中含有回环,Display 将会陷入死循环

    • 解决方法:采用 unsafe 的语言特性

编码为 S 表达式

  • S 表达式:采用 Lisp 语言的语法,被广泛使用。

通过 reflect.Value 修改值

  • 变量:一个可寻址的内存空间,里面存储了一个值,并且存储的值可以通过内存地址来更新。

    • x、 x.f[1]、 *p 是变量
    • x+2、f(2) 不是变量
  • 是否可取地址?是否允许修改?

    • reflect.Value 的 CanAddr()方法可以判断是否可取地址
    • reflect.Value 的 CanSet()方法可以判断是否允许修改
x := 2                   // value   type    variable?
a := reflect.ValueOf(2)  // 2       int     no 仅仅是整数2的拷贝副本
b := reflect.ValueOf(x)  // 2       int     no
c := reflect.ValueOf(&x) // &x      *int    no 只是一个指针&x的拷贝
d := c.Elem()            // 2       int     yes (x) c的解引用方式生成的,指向另一个变量,因此是可取地址的。
  • 从变量对应的可取地址的 reflect.Value 来访问变量的步骤
    • 使用 Addr().Interface()设置
    • 使用 Set 函数设置。【异常情况:对于一个引用 interface{}类型的 reflect.Value 调用 SetInt 会导致 panic 异常,即使那个 interface{}变量对于整数类型也不行。】
x := 2
d := reflect.ValueOf(&x).Elem()
// 方法1
px := d.Addr().Interface().(*int)
*px = 3
fmt.Println(x) // 3
// 方法2
d.Set(reflect.Value(4))
fmt.Println(x) // 4

// 异常情况
var y interface{}
ry := reflect.ValueOf(&y).Elem()
ry.SetInt(2)                     // panic: SetInt called on interface Value
ry.Set(reflect.ValueOf(3))       // OK, y = int(3)
ry.SetString("hello")            // panic: SetString called on interface Value
ry.Set(reflect.ValueOf("hello")) // OK, y = "hello"
  • 反射可以越过 Go 语言的导出规则,读取结构体中未导出的成员。
    • 一个可取地址的 reflect.Value 会记录一个结构体成员是否是未导出成员,如果是的话则拒绝修改操作。

解码 S 表达式

  • 4.5 节中的 Unmarshal 方法可以用于解码

反射存在的问题

  • 反射的代码比较脆弱

    • 反射则是在真正运行到的时候才会抛出 panic 异常,可能是写完代码很久之后了,而且程序也可能运行了很长的时间。
  • 反射的操作不能进行静态检查
  • 反射的代码比正常的代码运行慢一到两个数量级

7.接口 Interface

接口是合约

接口类型是对其它类型行为的抽象和概括;因为接口类型不会和特定的实现细节绑定在一起,通过这种抽象的方式我们可以让我们的函数更加灵活和更具有适应能力。

Go 语言中接口类型的独特之处在于它是满足隐式实现,没有必要对于给定的具体类型定义所有满足的接口类型;简单地拥有一些必需的方法就足够了。这种设计可以让你创建一个新的接口类型满足已经存在的具体类型却不会去改变这些类型的定义。

接口类型是一种抽象的类型。它不会暴露出它所代表的对象的内部值的结构和这个对象支持的基础操作的集合(当你有看到一个接口类型的值时,你不知道它是什么,唯一知道的就是可以通过它的方法来做什么。)

fmt.Fprintf 函数没有对具体操作的值做任何假设,而是仅仅通过 io.Writer 接口的约定来保证行为,所以第一个参数可以安全地传入一个只需要满足 io.Writer 接口的任意具体类型的值。一个类型可以自由地被另一个满足相同接口的类型替换,被称作可替换性(LSP 里氏替换)。这是一个面向对象的特征。

package io

// Writer is the interface that wraps the basic Write method.
type Writer interface {
    // Write writes len(p) bytes from p to the underlying data stream.
    // It returns the number of bytes written from p (0 <= n <= len(p))
    // and any error encountered that caused the write to stop early.
    // Write must return a non-nil error if it returns n < len(p).
    // Write must not modify the slice data, even temporarily.
    //
    // Implementations must not retain p.
    Write(p []byte) (n int, err error) // 如果一些具体的类型要继承这个interface, 需要实现这个签名为 Write(p []byte) (n int, err error)和行为的函数
}

package fmt

func Fprintf(w io.Writer, format string, args ...interface{}) (int, error)
func Printf(format string, args ...interface{}) (int, error) {
    return Fprintf(os.Stdout, format, args...)
}
func Sprintf(format string, args ...interface{}) string {
    var buf bytes.Buffer
    Fprintf(&buf, format, args...)
    return buf.String()
}

接口类型

package io
type Reader interface { // 表示对外暴露
    Read(p []byte) (n int, err error)
}


// 有些新的接口类型通过组合已有的接口来定义
// 目的是简洁地将Reader接口和Writer接口的签名组合在一起, 方便实现里氏替换
type ReadWriter interface {
    Reader
    Writer
}

Goroutines 和 Channels

  • Go 语言中的并发程序可以用两种手段来实现。本章讲解 goroutine 和 channel,其支持“顺序通信进程”(communicating sequential processes)或被简称为 CSP。CSP 是一种现代的并发编程模型,在这种编程模型中值会在不同的运行实例(goroutine)中传递,尽管大多数情况下仍然是被限制在单一实例中。

Goroutines

  • 当一个程序启动时,其主函数即在一个单独的 goroutine 中运行,我们叫它 main goroutine。新的 goroutine 会用 go 语句来创建。在语法上,go 语句是一个普通的函数或方法调用前加上关键字 go。go 语句会使其语句中的函数在一个新创建的 goroutine 中运行。而 go 语句本身会迅速地完成。
f()    // call f(); wait for it to return
go f() // create a new goroutine that calls f(); don't wait
  • 主函数返回时,所有的 goroutine 都会被直接打断,程序退出。除了从主函数退出或者直接终止程序之外,没有其它的编程方法能够让一个 goroutine 来打断另一个的执行,但是之后可以看到一种方式来实现这个目的,通过 goroutine 之间的通信来让一个 goroutine 请求其它的 goroutine,并让被请求的 goroutine 自行结束执行。

Channels

  • 如果说 goroutine 是 Go 语言程序的并发体的话,那么 channels 则是它们之间的通信机制。一个 channel 是一个通信机制,它可以让一个 goroutine 通过它给另一个 goroutine 发送值信息。每个 channel 都有一个特殊的类型,也就是 channels 可发送数据的类型。一个可以发送 int 类型数据的 channel 一般写为 chan int。
  • 一个 channel 有发送和接受两个主要操作,都是通信行为。一个发送语句将一个值从一个 goroutine 通过 channel 发送到另一个执行接收操作的 goroutine。发送和接收两个操作都使用 <- 运算符。在发送语句中,<- 运算符分割 channel 和要发送的值。在接收语句中,<- 运算符写在 channel 对象之前。一个不使用接收结果的接收操作也是合法的。
ch <- x  // a send statement
x = <-ch // a receive expression in an assignment statement
<-ch     // a receive statement; result is discarded
  • Channel 还支持 close 操作,用于关闭 channel,随后对基于该 channel 的任何发送操作都将导致 panic 异常。对一个已经被 close 过的 channel 进行接收操作依然可以接受到之前已经成功发送的数据;如果 channel 中已经没有数据的话将产生一个零值的数据。