golang圣经笔记(上) - Go语言中文社区

golang圣经笔记(上)


第一章节、入门

1.1一个简单的程序

package main

import "fmt"

func main() {
    fmt.Println("Hello, 世界")
}

命令行
$ go run helloworld.go

$ go build helloworld.go

Go语言不需要在语句或者声明的末尾添加分号,除非一行上有多条语句。
Go语言在代码格式上采取了很强硬的态度。gofmt工具把代码格式化为标准格式。

很多文本编辑器都可以配置为保存文件时自动执行gofmt,这样你的源代码总会被恰当地格式化。还有个相关的工具,goimports,可以根据代码需要, 自动地添加或删除import声明。
$ go get golang.org/x/tools/cmd/goimports

1.2命令行参数

os.Args变量是一个字符串(string)的切片(slice)
用s[i]访问单个元素,用s[m:n]获取子序列
序列的元素数目为len(s)
Go言里也采用左闭右开形式,比如s[m:n]这个切片,0 ≤ m ≤ n ≤ len(s),包含n-m个元素。
os.Args[1:len(os.Args)]切片中。如果省略切片表达式的m或n,会默认传入0或len(s),因此前面的切片可以简写成os.Args[1:]。

第二章节、程序结构

2.1命名

  1. 在习惯上,Go语言程序员推荐使用 驼峰式 命名,当名字有几个单词组成的时优先使用大小写分隔

2.2声明

  1. 一个函数的声明由一个函数名字、参数列表(由函数的调用者提供参数变量的具体值)、一个可选的返回值列表和包含函数定义的函数体组成。如果函数没有返回值,那么返回值列表是省略的。

2.3变量

  1. 一般用法:var 变量名字 类型 = 表达式
  2. 其中“类型”或“= 表达式”两个部分可以省略其中的一个
  3. 所有变量声明的同时都会做初始化。
  4. 也可以在一个声明语句中同时声明一组变量,或用一组初始化表达式声明并初始化一组变量。
var i, j, k int                 // int, int, int
var b, f, s = true, 2.3, "four" // bool, float64, string
  1. 可以用:= 声明或者初始化局部变量
  2. 请记住“:=”是一个变量声明语句加上赋值,而“=”是一个变量赋值操作。
  3. 在下面的代码中,第一个语句声明了in和err两个变量。在第二个语句只声明了out一个变量,然后对已经声明的err进行了赋值操作。
in, err := os.Open(infile)
// ...
out, err := os.Create(outfile)
  1. 同一个变量只能声明一次。
  2. 指针的使用基本和C一致,有用&表示地址,*表示指针。
  3. p := new(int)用new新建一个int型指针
  4. 变量的生命周期和c的不同,如果return了一个指针,那么在函数结束之后,这个指针仍旧存在。golang分配堆还是栈的是由自己决定的,不是说用new就一定是堆,当golang检测到这个变量没有再使用就会释放内存。如果将指向短生命周期对象的指针保存到具有长生命周期的对象中,特别是保存到全局变量时,会阻止对短生命周期对象的垃圾回收

2.4赋值

  1. count[x] = count[x] * scale // 数组、slice或map的元素赋值
  2. 它允许同时更新多个变量的值
    x, y = y, x
    a[i], a[j] = a[j], a[i]
  3. 和变量声明一样,我们可以用下划线空白标识符_来丢弃不需要的值。
_, ok = x.(T)              // 只检测类型,忽略具体值```

###2.5类型
1. 类型重定义:```type 类型名字 底层类型```
2. 类型转换:```类型名字(参数)```
3. 只有底层类型相同,或者底层类型可以互相转换的类,才可以做类型转换。
4. 类型不同(就算底层类型相同)也不能做比较运算
5. 下面的声明语句,Celsius类型的参数c出现在了函数名的前面,表示声明的是Celsius类型的一个名叫String的方法,该方法返回该类型对象c带着°C温度单位的字符串:
```func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }```





<div class="se-preview-section-delimiter"></div>

###2.6包和文件
1. 在Go语言中,一个简单的规则是:如果一个名字是大写字母开头的,那么该名字是导出的。也就是说别的包,只要引用了改包就可以使用的参数。
``fmt.Printf("Brrrr! %vn", tempconv.AbsoluteZeroC) // "Brrrr! -273.15°C"``
2. 包内函数的使用:```fmt.Println(tempconv.CToF(tempconv.BoilingC)) // "212°F"```,注意只有引用包的文件可以调用函数。
3. 在Go语言程序中,每个包都有一个全局唯一的导入路径。导入语句中类似"gopl.io/ch2/tempconv"的字符串对应包的导入路径。




<div class="se-preview-section-delimiter"></div>

```go
import(
    "gopl.io/ch2/tempconv"
)
  1. 如果导入了一个包,但是又没有使用该包将被当作一个编译错误处理。我们可以使用golang.org/x/tools/cmd/goimports导入工具解决冲突
  2. 可以用一个特殊的init初始化函数来简化初始化工作。每个文件都可以包含多个init初始化函数
    func init() {/*...*/}
  3. 初始化顺序:每个包在解决依赖的前提下,以导入声明的顺序初始化,每个包只会被初始化一次。因此,如果一个p包导入了q包,那么在p包初始化的时候可以认为q包必然已经初始化过了。初始化工作是自下而上进行的,main包最后被初始化。以这种方式,可以确保在main函数执行之前,所有依赖的包都已经完成初始化工作了。
  4. 除了可以使用init初始化以外,也可以使用匿名函数进行初始化,下面两种方式是等价的
//使用init初始化
package popcount

// pc[i] is the population count of i.
var pc [256]byte

func init() {
    for i := range pc {
        pc[i] = pc[i/2] + byte(i&1)
    }
}

//或者使用匿名函数初始化
// pc[i] is the population count of i.
var pc [256]byte = func() (pc [256]byte) {
    for i := range pc {
        pc[i] = pc[i/2] + byte(i&1)
    }
    return
}()

2.7作用域

  1. 当编译器遇到一个名字引用时,如果它看起来像一个声明,它首先从最内层的词法域向全局的作用域查找。如果查找失败,则报告“未声明的名字”这样的错误。如果该名字在内部和外部的块分别声明过,则内部块的声明首先被找到。在这种情况下,内部声明屏蔽了外部同名的声明,让外部的声明的名字无法被访问。
  2. 在函数中词法域可以深度嵌套,因此内部的一个声明可能屏蔽外部的声明。还有许多语法块是if或for等控制流语句构造的。下面的代码有三个不同的变量x,因为它们是定义在不同的词法域(这个例子只是为了演示作用域规则,但不是好的编程风格)。
func main() {
    x := "hello!"
    for i := 0; i < len(x); i++ {
        x := x[i]
        if x != '!' {
            x := x + 'A' - 'a'
            fmt.Printf("%c", x) // "HELLO" (one letter per iteration)
        }
    }
}
  1. 会在for、if、switch语句等条件部分创建隐式词法域。
  2. if语句嵌套在第一个内部,因此第一个if语句条件初始化词法域声明的变量在第二个if中也可以访问。
if x := f(); x == 0 {
    fmt.Println(x)
} else if y := g(x); x == y {
    fmt.Println(x, y)
} else {
    fmt.Println(x, y)
}
fmt.Println(x, y) // compile error: x and y are not visible here
  1. 如果一个变量或常量递归引用了自身,则会产生编译错误。
  2. 变量f的作用域只有在if语句内,因此后面的语句将无法引入它,这将导致编译错误。你可能会收到一个局部变量f没有声明的错误提示,具体错误信息依赖编译器的实现。
if f, err := os.Open(fname); err != nil { // compile error: unused: f
    return err
}
f.ReadByte() // compile error: undefined f
f.Close()    // compile error: undefined f

第三章、基础数据类型

3.5字符串

  1. 字符串的值是不可变的:一个字符串包含的字节序列永远不会被改变。
  2. 因为字符串是不可修改的,因此尝试修改字符串内部数据的操作也是被禁止的。
  3. 一个原生的字符串面值形式是“,使用反引号代替双引号。
  4. Go语言的range循环在处理字符串的时候,会自动隐式解码UTF8字符串。
  5. 将[]rune类型转换应用到UTF8编码的字符串,将返回字符串编码的Unicode码点序列:
// "program" in Japanese katakana
s := "プログラム"
fmt.Printf("% xn", s) // "e3 83 97 e3 83 ad e3 82 b0 e3 83 a9 e3 83 a0"
r := []rune(s)
fmt.Printf("%xn", r)  // "[30d7 30ed 30b0 30e9 30e0]"

如果是将一个[]rune类型的Unicode字符slice或数组转为string,则对它们进行UTF8编码:

fmt.Println(string(r)) // "プログラム"
  1. 将一个整数转型为字符串意思是生成以只包含对应Unicode码点字符的UTF8字符串:
fmt.Println(string(65))     // "A", not "65"
fmt.Println(string(0x4eac)) // "京"
  1. 标准库中有四个包对字符串处理尤为重要:bytes、strings、strconv和unicode包.
  2. bytes包也提供了很多类似功能的函数,但是针对和字符串有着相同结构的[]byte类型。
  3. strconv包提供了布尔型、整型数、浮点数和对应字符串的相互转换,还提供了双引号转义相关的转换。
  4. unicode包提供了IsDigit、IsLetter、IsUpper和IsLower等类似功能,它们用于给字符分类。

3.6常量

  1. 对常量的类型转换操作或以下函数调用都是返回常量结果。
  2. 如果是批量声明的常量,除了第一个外其它的常量右边的初始化表达式都可以省略,如果省略初始化表达式则表示使用前面常量的初始化表达式写法,对应的常量类型也一样的。例如:
const (
    a = 1
    b
    c = 2
    d
)

fmt.Println(a, b, c, d) // "1 1 2 2"
  1. 常量声明可以使用iota常量生成器初始化,iota将会被置为0,然后在每一个有常量声明的行加一。
type Weekday int

const (
    Sunday Weekday = iota
    Monday
    Tuesday
    Wednesday
    Thursday
    Friday
    Saturday
)
  1. 许多常量并没有一个明确的基础类型。这里有六种未明确类型的常量类型,分别是无类型的布尔型、无类型的整数、无类型的字符、无类型的浮点数、无类型的复数、无类型的字符串。
  2. 常量的形式将隐式决定变量的默认类型,无类型整数常量转换为int,它的内存大小是不确定的,但是无类型浮点数和复数常量则转换为内存大小明确的float64和complex128。

第四章、复合数据类型

4.1数组

  1. 数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。
var a [3]int             // array of 3 integers
fmt.Println(a[0])        // print the first element
fmt.Println(a[len(a)-1]) // print the last element, a[2]
  1. 默认情况下,数组的每个元素都被初始化为元素类型对应的零值,对于数字类型来说就是0。
  2. 如果一个数组的元素类型是可以相互比较的,那么数组类型也是可以相互比较的,这时候我们可以直接通过==比较运算符来比较两个数组。

4.2Slice切片

  1. 多个slice之间可以共享底层的数据,并且引用的数组部分区间可能重叠。
  2. 数据结构:
    Alt text
  3. 如果切片操作超出cap(s)的上限将导致一个panic异常,但是超出len(s)则是意味着扩展了slice,因为新slice的长度会变大:
fmt.Println(summer[:20]) // panic: out of range

endlessSummer := summer[:5] // extend a slice (within capacity)
fmt.Println(endlessSummer)  // "[June July August September October]"
  1. 和数组不同的是,slice之间不能比较,因此我们不能使用==操作符来判断两个slice是否含有全部相等元素。标准库提供了高度优化的bytes.Equal函数来判断两个字节型slice是否相等([]byte),但是对于其他类型的slice,我们必须自己展开每个元素进行比较。
  2. slice唯一合法的比较操作是和nil比较。
  3. 如果你需要测试一个slice是否是空的,使用len(s) == 0来判断,而不应该用s == nil来判断。
  4. 在底层,make创建了一个匿名的数组变量,然后返回一个slice;只有通过返回的slice才能引用底层匿名的数组变量。
make([]T, len)
make([]T, len, cap) // same as make([]T, cap)[:len]
  1. 内置的append函数用于向slice追加元素:
var runes []rune
for _, r := range "Hello, 世界" {
    runes = append(runes, r)
}
fmt.Printf("%qn", runes) // "['H' 'e' 'l' 'l' 'o' ',' ' ' '世' '界']"
  1. 我们不能确认在原先的slice上的操作是否会影响到新的slice。因此,通常是将append返回的结果直接赋值给输入的slice变量:runes = append(runes, r)
  2. 一个slice可以用来模拟一个stack。最初给定的空slice对应一个空的stack,然后可以使用append函数将新的值压入stack:
    stack = append(stack, v) // push v
    stack的顶部位置对应slice的最后一个元素:
    top := stack[len(stack)-1] // top of stack
    通过收缩stack可以弹出栈顶的元素
    stack = stack[:len(stack)-1] // pop

4.3Map

  1. 内置的make函数可以创建一个map:ages := make(map[string]int) // mapping from strings to ints
  2. 即使map中不存在“bob”下面的代码也可以正常工作,因为ages[“bob”]失败时将返回0。
  3. Map遍历的顺序是随机的,每一次遍历的顺序都不相同。
  4. map类型的零值是nil,也就是没有引用任何哈希表。向一个nil值的map存入元素将导致一个panic异常,在向map存数据前必须先创建map。
  5. 开始就知道names的最终大小,因此给slice分配一个合适的大小将会更有效names := make([]string, 0, len(ages))
  6. 判断key是否存在:
age, ok := ages["bob"]
if !ok { /* "bob" is not a key in this map; age == 0. */ }
  1. 可以用map实现类似set的功能:map[string]bool

4.4结构体

  1. 通常一行对应一个结构体成员,成员的名字在前类型在后,不过如果相邻的成员类型如果相同的话可以被合并到一行。
type Employee struct {
    ID            int
    Name, Address string
    DoB           time.Time
    Position      string
    Salary        int
    ManagerID     int
}
  1. 如果结构体成员名字是以大写字母开头的,那么该成员就是导出的。其他包只能访问结构体内导出的参数。
  2. 一个命名为S的结构体类型将不能再包含S类型的成员:因为一个聚合的值不能包含它自身。但是S类型的结构体可以包含*S指针类型的成员
  3. 二叉树实现插入排序(这个写法很有意思):
type tree struct {
    value       int
    left, right *tree
}

// Sort sorts values in place.
func Sort(values []int) {
    var root *tree
    for _, v := range values {
        root = add(root, v)
    }
    appendValues(values[:0], root)
}

// appendValues appends the elements of t to values in order
// and returns the resulting slice.
func appendValues(values []int, t *tree) []int {
    if t != nil {
        values = appendValues(values, t.left)
        values = append(values, t.value)
        values = appendValues(values, t.right)
    }
    return values
}

func add(t *tree, value int) *tree {
    if t == nil {
        // Equivalent to return &tree{value: value}.
        t = new(tree)
        t.value = value
        return t
    }
    if value < t.value {
        t.left = add(t.left, value)
    } else {
        t.right = add(t.right, value)
    }
    return t
}
  1. 结构体字面值可以指定每个成员的值。
type Point struct{ X, Y int }
p := Point{1, 2}
p := Point{a:1, b:2}
pp := &Point{1, 2}//这里pp是指针
pp := new(Point)
*pp = Point{1, 2}
  1. 你不能企图在外部包中用第一种顺序赋值的技巧来偷偷地初始化结构体中未导出的成员。
  2. 如果考虑效率的话,较大的结构体通常会用指针的方式传入和返回。在Go语言中,所有的函数参数都是值拷贝传入的,函数参数将不再是函数调用时的原始变量。
  3. 如果结构体的全部成员都是可以比较的,那么结构体也是可以比较的,那样的话两个结构体将可以使用==或!=运算符进行比较。
  4. 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
  1. 结构体字面值并没有简短表示匿名成员的语法, 因此下面的语句都不能编译通过:
w = Wheel{8, 8, 5, 20}                       // compile error: unknown fields
w = Wheel{X: 8, Y: 8, Radius: 5, Spokes: 20} // compile error: unknown fields
  1. 不能同时包含两个类型相同的匿名成员,这会导致名字冲突。

第五章、函数

5.1函数声明

  1. 实参通过值的方式传递,因此函数的形参是实参的拷贝。对形参进行修改不会影响实参。但是,如果实参包括引用类型,如指针,slice(切片)、map、function、channel等类型,实参可能会由于函数的间接引用被修改。

5.2递归

  1. 大部分编程语言使用固定大小的函数调用栈,常见的大小从64KB到2MB不等。固定大小栈会限制递归的深度,当你用递归处理大量数据时,需要避免栈溢出;除此之外,还会导致安全性问题。与相反,Go语言使用可变栈,栈的大小按需增加(初始时很小)。这使得我们使用递归时不必考虑溢出和安全问题。

5.3多返回值

  1. 如果一个函数将所有的返回值都显示的变量名,那么该函数的return语句可以省略操作数。这称之为bare return。
/ CountWordsAndImages does an HTTP GET request for the HTML
// document url and returns the number of words and images in it.
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) { /* ... */ }
  1. bare return 可以减少代码的重复,但是使得代码难以被理解。不宜过度使用bare return。

5.4错误

  1. Go使用控制流机制(如if和return)处理异常, 而不是用抛出异常。
  2. 五种错误处理方式:
    1) 直接错误返回给调用者,或者格式化错误信息并返回return nil, fmt.Errorf("parsing %s as HTML: %v", url,err)
    2) 重新尝试失败的操作,需要对重试次数和时间做限制
// WaitForServer attempts to contact the server of a URL.
// It tries for one minute using exponential back-off.
// It reports an error if all attempts fail.
func WaitForServer(url string) error {
    const timeout = 1 * time.Minute
    deadline := time.Now().Add(timeout)
    for tries := 0; time.Now().Before(deadline); tries++ {
        _, err := http.Head(url)
        if err == nil {
            return nil // success
        }
        log.Printf("server not responding (%s);retrying…", err)
        time.Sleep(time.Second << uint(tries)) // exponential back-off
    }
    return fmt.Errorf("server %s failed to respond after %s", url, timeout)
}

3) 输出错误信息并结束程序。
4)可以通过log包提供函数,或者标准错误流输出错误信息。

if err := Ping(); err != nil {
    log.Printf("ping failed: %v; networking disabled",err)
}
if err := Ping(); err != nil {
    fmt.Fprintf(os.Stderr, "ping failed: %v; networking disabledn", err)
}

5) 直接忽略

5.5函数值

  1. 函数类型的零值是nil。调用值为nil的函数值会引起panic错误。
  2. 函数值之间是不可比较的,也不能用函数值作为map的key。

5.6匿名函数(比较重要)

  1. 匿名函数可以在函数中被定义,并且这种方式定义的函数可以访问完整的词法环境,这意味着在函数中定义的内部函数可以引用该函数的变量。
// 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"
}
  1. 匿名函数内使用递归的话,就一定要先声明函数,再赋值函数。下面
var visitAll func(items []string)//第一步声明
visitAll = func(items []string) {//第二步定义
    ...}
}

5.7可变参数

  1. 在声明可变参数函数时,需要在参数列表的最后一个参数类型之前加上省略符号“…”,这表示该函数会接收任意数量的该类型参数。
func sum(vals...int) int {
    total := 0
    for _, val := range vals {
        total += val
    }
    return total
}
  1. 可以把数组的一个切片作为参数传给被调函数。
    values := []int{1, 2, 3, 4}
    fmt.Println(sum(values...)) // "10"
  2. 虽然在可变参数函数内部,…int 型参数的行为看起来很像切片类型,但实际上,可变参数函数和以切片作为参数的函数是不同的。
  3. interfac{}表示函数的最后一个参数可以接收任意类型。

5.8Deferred函数

  1. 在命令行之前使用defer,会在函数return或者异常退出之后运行:
    defer resp.Body.Close()
  2. defer语句会在函数退出但是销毁栈之前运行。
  3. defer语句可以加一个返回函数的函数,会在改语句除运行改函数,再退出时使用返回的函数。
func bigSlowOperation() {
    defer trace("bigSlowOperation")() // don't forget the
    extra parentheses
    // ...lots of work…
    time.Sleep(10 * time.Second) // simulate slow
    operation by sleeping
}
func trace(msg string) func() {
    start := time.Now()
    log.Printf("enter %s", msg)
    return func() { 
        log.Printf("exit %s (%s)", msg,time.Since(start)) 
    }
}

$ go build gopl.io/ch5/trace
$ ./trace
2015/11/18 09:53:26 enter bigSlowOperation
2015/11/18 09:53:36 exit bigSlowOperation (10.000589217s)
  1. 对匿名函数采用defer机制,可以使其观察函数的返回值。
func double(x int) int {
    return x + x
}

5.9Panic异常

  1. 一般而言,当panic异常发生时,程序会中断运行,并立即执行在该goroutine(可以先理解成线程,在第8章会详细介绍)中被延迟的函数(defer 机制)。随后,程序崩溃并输出日志信息。
  2. 尽量不要主动使用panic异常panic(err)

5.10Recover捕获异常

  1. 如果在deferred函数中调用了内置函数recover,并且定义该defer语句的函数发生了panic异常,recover会使程序从panic中恢复,并返回panic value。导致panic异常的函数不会继续运行,但能正常返回。在未发生panic时调用recover,recover会返回nil。
  2. 尽量不要使用,因为异常可能会导致资源管理有漏洞,尽量只针对少数几种异常恢复,如下:
// soleTitle returns the text of the first non-empty title element
// in doc, and an error if there was not exactly one.
func soleTitle(doc *html.Node) (title string, err error) {
    type bailout struct{}
    defer func() {
        switch p := recover(); p {
        case nil:       // no panic
        case bailout{}: // "expected" panic
            err = fmt.Errorf("multiple title elements")
        default:
            panic(p) // unexpected panic; carry on panicking
        }
    }()
    // Bail out of recursion if we find more than one nonempty title.
    forEachNode(doc, func(n *html.Node) {
        if n.Type == html.ElementNode && n.Data == "title" &&
            n.FirstChild != nil {
            if title != "" {
                panic(bailout{}) // multiple titleelements
            }
            title = n.FirstChild.Data
        }
    }, nil)
    if title == "" {
        return "", fmt.Errorf("no title element")
    }
    return title, nil
}
  1. 有些情况下,我们无法恢复。某些致命错误会导致Go在运行时终止程序,如内存不足。

第六章、方法

6.1方法声明

  1. 举例:
type Point struct{ X, Y float64 }

// same thing, but as a method of the Point type
func (p Point) Distance(q Point) float64 {
    return math.Hypot(q.X-p.X, q.Y-p.Y)
}
  1. 由于方法和字段都是在同一命名空间,所以如果我们在这里声明一个X方法的话,编译器会报错,因为在调用p.X时会有歧义.

6.2基于指针对象方法

  1. 当这个接受者变量本身比较大时,我们就可以用其指针而不是对象来声明方法,如下:
    func (p *Point) ScaleBy(factor float64) {
    p.X *= factor
    p.Y *= factor
    }
  2. 在现实的程序里,一般会约定如果Point这个类有一个指针作为接收器的方法,那么所有Point的方法都必须有一个指针接收器。
  3. 如果一个类型名本身是一个指针的话,是不允许其出现在接收器中的。
    type P *int
    func (P) f() { /* ... */ } // compile error: invalid receiver type
  4. 编译器会隐式地帮我们用&p去调用ScaleBy这个方法。((&p).ScaleBy(2)可以这么表达p.ScaleBy(2))但是我们不能通过一个无法取到地址的接收器来调用指针方法,比如临时变量的内存地址就无法获取得到:Point{1, 2}.ScaleBy(2) // compile error: can't take address of Point literal
  5. 编译器在这里也会给我们隐式地插入*这个操作符,所以下面这两种写法等价的:
    pptr.Distance(q)
    (*pptr).Distance(q)
  6. 但是如果一个方法使用指针作为接收器,你需要避免对其进行拷贝,因为这样可能会破坏掉该类型内部的不变性。对拷贝后的变量进行修改可能会有让你意外的结果。

6.3通过嵌入结构体来扩展类型

  1. 举例:
import "image/color"

type Point struct{ X, Y float64 }

type ColoredPoint struct {
    Point
    Color color.RGBA
}
  1. 可以继承结构体中所有结构体对应的函数,有点类似继承的关系。按上面的例子,对于Point中的方法我们也有类似的用法,我们可以把ColoredPoint类型当作接收器来调用Point里的方法。
  2. 需要注意,必须p.Distance(q.Point)调用,这样是错的p.Distance(q), 但是可以重新针对ColoredPoint定义Distance方法。
  3. 在类型中内嵌的匿名字段也可能是一个命名类型的指针,这种情况下字段和方法会被间接地引入到当前的类型中(译注:访问需要通过该指针指向的对象去取)。
type ColoredPoint struct {
    *Point
    Color color.RGBA
}

p := ColoredPoint{&Point{1, 1}, red}
q := ColoredPoint{&Point{5, 4}, blue}
fmt.Println(p.Distance(*q.Point)) // "5"
q.Point = p.Point                 // p and q now share the same Point
p.ScaleBy(2)
fmt.Println(*p.Point, *q.Point) // "{2 2} {2 2}"
  1. 它会首先去找直接定义在这个类型里的方法,然后再去内嵌字段的类型中去找方法。
  2. 选择器有二义性的话编译器会报错,比如你在同一级里有两个同名的方法。
  3. 给匿名struct类型来定义方法,还可以初始化。
    var cache = struct {
    sync.Mutex
    mapping map[string]string
    }{
    mapping: make(map[string]string),
    }

6.4方法值和方法表达式

  1. 可以把某个类中某一个方法赋值给变量
    distanceFromP := p.Distance // method value
    fmt.Println(distanceFromP(q)) // "5"
  2. 当T是一个类型时,方法表达式可能会写作T.f或者(*T).f,会返回一个函数”值”,这种函数会将其第一个参数用作接收器。
p := Point{1, 2}
q := Point{4, 6}

distance := Point.Distance   // method expression
fmt.Println(distance(p, q))  // "5"
fmt.Printf("%Tn", distance) // "func(Point, Point) float64"
scale := (*Point).ScaleBy
scale(&p, 2)
fmt.Println(p)            // "{2 4}"
fmt.Printf("%Tn", scale) // "func(*Point, float64)"

// 译注:这个Distance实际上是指定了Point对象为接收器的一个方法func (p Point) Distance(),
// 但通过Point.Distance得到的函数需要比实际的Distance方法多一个参数,
// 即其需要用第一个额外参数指定接收器,后面排列Distance方法的参数。
// 看起来本书中函数和方法的区别是指有没有接收器,而不像其他语言那样是指有没有返回值。
  1. 下面就是一个比较有意思的例子,会根据不同的输入绑定不同的方法
ype Point struct{ X, Y float64 }

func (p Point) Add(q Point) Point { return Point{p.X + q.X, p.Y + q.Y} }
func (p Point) Sub(q Point) Point { return Point{p.X - q.X, p.Y - q.Y} }

type Path []Point

func (path Path) TranslateBy(offset Point, add bool) {
    var op func(p, q Point) Point
    if add {
        op = Point.Add
    } else {
        op = Point.Sub
    }
    for i := range path {
        // Call either path[i].Add(offset) or path[i].Sub(offset).
        path[i] = op(path[i], offset)
    }
}

第七章、接口

7.2接口类型

  1. 普通接口:
package io
type Reader interface {
    Read(p []byte) (n int, err error)
}
type Closer interface {
    Close() error
}
  1. 内嵌型:
type ReadWriter interface {
    Reader
    Writer
}
  1. 上面两种类型可以混合

7.3实现接口的条件

  1. 一个类型如果拥有一个接口需要的所有方法,那么这个类型就实现了这个接口。如果没有完全实现方法,在接口赋值的时候会报错。
  2. 如果方法返回的是*T指针的话,是不能使用var _ = IntSet{}.String()这种方式寻址的(String()是接口实现的方法),报错compile error: String requires *IntSet receiver但是可以用变量调用var s IntSet;var _ = s.String()
  3. interface{}被称为空接口类型,空接口类型对实现它的类型没有要求,所以我们可以将任意一个值赋给空接口类型。

7.4flag.Value接口

  1. flag.Value用来管理命令行参数的
  2. 一旦实现了下面这个接口下面的方法,就可以用f := 实现接口类型{value参数};flag.CommandLine.Var(&f, name, usage)加入名两行参数
type Value interface {
    String() string
    Set(string) error
}
  1. 使用flag.Parse()打印命令行参数

7.5. 接口值

  1. 在Go语言中,变量总是被一个定义明确的值初始化,即使接口类型也不例外。对于一个接口的零值就是它的类型和值的部分都是nil。(接口值是一个指针指向的内存空间)
    var w io.Writer:
    Alt text
  2. 复制过程,指针值的type和value都会发生变化
    w = os.Stdout
    Alt text
    1. 接口值可以使用==和!=来进行比较。如果两个接口值的动态类型相同,但是这个动态类型是不可比较的(比如切片),将它们进行比较就会失败并且panic。在比较接口值或者包含了接口值的聚合类型时,我们必须要意识到潜在的panic。
    2. 一个不包含任何值的nil接口值和一个刚好包含nil指针的接口值是不同的。var buf *bytes.Buffer这个接口值,指向的内容不是nil,但是参数value是nil,所以在执行有些参数不能为nil的方法时也会报错的。尽量不要用这种方式。
      Alt text

7.6sort.Interface接口

  1. 用来排序的接口,需要完成一下接口的定义才能使用:
package sort

type Interface interface {
    Len() int
    Less(i, j int) bool // i, j are indices of sequence elements
    Swap(i, j int)
}

举例:

type StringSlice []string
func (p StringSlice) Len() int           { return len(p) }
func (p StringSlice) Less(i, j int) bool { return p[i] < p[j] }
func (p StringSlice) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }

sort.Sort(StringSlice(names))//执行排序
  1. sort.Reverse函数是逆向排序,注意!结构体reverse嵌入了一个sort.Interface,这样就可以直接调用interface中的方法,对自己的接口方法重构
package sort

type reverse struct{ Interface } // that is, sort.Interface

func (r reverse) Less(i, j int) bool { return r.Interface.Less(j, i) }

func Reverse(data Interface) Interface { return reverse{data} }

sort.Sort(sort.Reverse(接口值))

7.7. http.Handler接口

  1. 一个标准的http服务端程序
func main() {
    db := database{"shoes": 50, "socks": 5}
    log.Fatal(http.ListenAndServe("localhost:8000", db))
}

type dollars float32

func (d dollars) String() string { return fmt.Sprintf("$%.2f", d) }

type database map[string]dollars

func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    switch req.URL.Path {
    case "/list":
        for item, price := range db {
            fmt.Fprintf(w, "%s: %sn", item, price)
        }
    case "/price":
        item := req.URL.Query().Get("item")
        price, ok := db[item]
        if !ok {
            w.WriteHeader(http.StatusNotFound) // 404
            fmt.Fprintf(w, "no such item: %qn", item)
            return
        }
        fmt.Fprintf(w, "%sn", price)
    default:
        w.WriteHeader(http.StatusNotFound) // 404
        fmt.Fprintf(w, "no such page: %sn", req.URL)
    }
}
  1. 使用ServeMux:
func main() {
    db := database{"shoes": 50, "socks": 5}
    mux := http.NewServeMux()
    mux.Handle("/list", http.HandlerFunc(db.list))
    mux.Handle("/price", http.HandlerFunc(db.price))
    log.Fatal(http.ListenAndServe("localhost:8000", mux))
}

type database map[string]dollars

func (db database) list(w http.ResponseWriter, req *http.Request) {
    for item, price := range db {
        fmt.Fprintf(w, "%s: %sn", item, price)
    }
}

func (db database) price(w http.ResponseWriter, req *http.Request) {
    item := req.URL.Query().Get("item")
    price, ok := db[item]
    if !ok {
        w.WriteHeader(http.StatusNotFound) // 404
        fmt.Fprintf(w, "no such item: %qn", item)
        return
    }
    fmt.Fprintf(w, "%sn", price)
}

中间db.list和db.price都是方法值,语句http.HandlerFunc(db.list)是一个转换而非一个函数调用,因为http.HandlerFunc是一个类型

package http

type HandlerFunc func(w ResponseWriter, r *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

注意这里HandlerFunc类型是一个函数,这种

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/fanfansong123/article/details/80678774
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2019-08-25 14:26:27
  • 阅读 ( 1199 )
  • 分类:Go

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢