GO语言圣经笔记|青训营笔记

88 阅读7分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第4天

GO语言圣经笔记

1. 入门

1.3更多类型

指针

Go 拥有指针。指针保存了值的内存地址。

类型*T是指向T类型值的指针。其零值为nilvar p *int

& 操作符会生成一个指向其操作数的指针。

* 操作符表示指针指向的底层值。

与 C 不同,Go 没有指针运算。

 package main
 ​
 import "fmt"
 ​
 func main() {
         i, j := 42, 2701
 ​
         p := &i         // 指向 i
         fmt.Println(*p) // 通过指针读取 i 的值
         fmt.Println(p)  // 读取指针变量p存储的 i 的地址
         fmt.Println(&i) // i 的地址
         fmt.Println(&p) // 指针变量p的存储地址
         *p = 21         // 通过指针设置 i 的值
         fmt.Println(i)  // 查看 i 的值
 ​
         p = &j         // 指向 j
         *p = *p / 37   // 通过指针对 j 进行除法运算
         fmt.Println(j) // 查看 j 的值
 }

* 操作符表示指针指向的底层值。

结构体

一个结构体(struct)就是一组字段(field),结构体字段使用点号来访问。

结构体字段可以通过结构体指针来访问。如果我们有一个指向结构体的指针p,那么可以通过(*p).X来访问其字段 X。不过这么写太啰嗦了,所以语言也允许我们使用隐式间接引用,直接写 p.X 就可以。

 package main
 ​
 import "fmt"
 ​
 type Vertex struct {
         X int
         Y int
 }
 ​
 func main() {
         v := Vertex{1, 2} //定义v为结构体变量
         fmt.Println(v)
         v.X = 4
         fmt.Println(v.X)
     p := &v
     p.X = 1e9
     fmt.Println(v)
 }

C语言的结构体通常使用的是如下结构

 struct 结构体名 {
   成员表列
 }变量名表列;

在C语言中,为了避免结构体指针(*p).X的麻烦,使用p->X方法替代。

其中fmt.Println(v)的输出为{1 2},对于为什么会出现花括号这种输出格式,目前猜测为结构体的默认返回值是花括号加上参数值,如同后文输出数组和切片时为中括号加参数值,是一种固定的输出方法。

结构体语法

结构体文法通过直接列出字段的值来新分配一个结构体。

使用 Name: 语法可以仅列出部分字段。(字段名的顺序无关。)

特殊的前缀 & 返回一个指向结构体的指针。

 package main
 ​
 import "fmt"
 ​
 type Vertex struct {
   X, Y int
 }
 ​
 var (
   v1 = Vertex{1, 2}  // 创建一个 Vertex 类型的结构体
   v2 = Vertex{X: 1}  // Y:0 被隐式地赋予
   v3 = Vertex{}      // X:0 Y:0
   p  = &Vertex{1, 2} // 创建一个 *Vertex 类型的结构体(指针)
 )
 ​
 func main() {
   fmt.Println(v1, p, v2, v3)
 }

数组

类似于C。

类型[n]T表示拥有nT类型的值的数组。

表达式var a [10]int,会将变量a声明为拥有10个整数的数组。下标从0开始

数组的长度是其类型的一部分,因此数组不能改变大小。

切片(slice)

每个数组的大小都是固定的。而切片则为数组元素提供动态大小的、灵活的视角。在实践中,切片比数组更常用。

数组和slice之间有着紧密的联系,slice的语法和数组很像,只是没有固定长度而已。

slice提供了访问数组子序列(或者全部)元素的功能,而且slice的底层确实引用一个数组对象。

切片语法

类型[]T表示一个元素类型为T的切片。

数组的语法:[3]bool{true, true, false}

切片的语法:[]bool{true, true, false},这样则会创建一个和上面相同的数组,然后构建一个引用了它的切片

切片通过两个下标来界定,即一个上界和一个下界,二者以冒号分隔:a[low : high]

它是左闭右开区间,包括第一个元素,但排除最后一个元素。

例:以下表达式创建了一个切片,它包含a中下标从1到3的元素:a[1:4]

切片并不存储任何数据,它只是描述了底层数组中的一段。更改切片的元素会修改其底层数组中对应的元素

 package main
 ​
 import "fmt"
 ​
 func main() {
         primes := [6]int{2, 3, 5, 7, 11, 13}
         fmt.Println("primes数组的值:", primes)
         var s []int = primes[1:4]
         fmt.Println("[1:4]切片s的值:", s)
         s[2] = 1818
         fmt.Println("s[2]=1818后切片s的值:", s)
         fmt.Println("primes数组的值:", primes)
   
         p := []struct {
                 i int
                 b bool
         }{
                 {2, true},
                 {3, false},
                 {5, true},
                 {7, true},
                 {11, false},
                 {13, true},
         }
         fmt.Println(p)
 }

切片的默认行为

在进行切片时,可以利用它的默认行为来忽略上下界。切片下界的默认值为 0上界则是该切片的长度

数组var a [10]int与这些切片等价a[0:10]a[:10]a[0:]a[:]

切片的构成

一个slice由三个部分构成:指针、长度和容量。

指针:指针指向第一个slice元素对应的底层数组元素的地址(不一定就是数组的第一个元素)。

长度slice中元素的数目,长度不能超过容量。

容量:从slice的开始位置到底层数据的结尾位置。内置的len(s)cap(s)函数分别返回slice的长度和容量。

 package main
 ​
 import "fmt"
 ​
 func main() {
         s := []int{2, 3, 5, 7, 11, 13}
         printSlice(s)
 ​
         // 截取切片使其长度为 0
         s = s[:0]
         printSlice(s)
 ​
         // 拓展其长度
         s = s[:4]
         printSlice(s)
 ​
         // 舍弃前两个值
         s = s[2:]
         printSlice(s)
 ​
         // nil切片
         var p []int
         fmt.Println(p, len(p), cap(p))
         if p == nil {
                 fmt.Println("nil!")
         }
 }
 ​
 func printSlice(s []int) {
         fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
 }

nil 切片

切片的零值是nil

nil切片的长度和容量为0且没有底层数组。

用 make 创建切片

切片可以用内建函数make来创建,这也是创建动态数组的方式。

make函数会分配一个元素为零值的数组返回一个引用了它的切片

 package main
 ​
 import "fmt"
 ​
 func main() {
   a := make([]int, 5)   //输出:a len=5 cap=5 [0 0 0 0 0]
   printSlice("a", a)
 ​
   b := make([]int, 0, 5)
   printSlice("b", b)    //输出:b len=0 cap=5 []
   //b = b[:cap(b)]        len(b)=5, cap(b)=5
   
   c := b[:2]
   printSlice("c", c)    //输出:c len=2 cap=5 [0 0]
 ​
   d := c[2:5]
   printSlice("d", d)    //输出:d len=3 cap=3 [0 0 0]
 }
 ​
 func printSlice(s string, x []int) {
   fmt.Printf("%s len=%d cap=%d %v\n",
     s, len(x), cap(x), x)
 }

切片的切片

切片可包含任何类型,甚至包括其它的切片。

 package main
 ​
 import (
         "fmt"
         "strings"
 )
 ​
 func main() {
         // 创建一个井字板(经典游戏)
         board := [][]string{
                 []string{"_", "_", "_"},
                 []string{"_", "_", "_"},
                 []string{"_", "_", "_"},
         }
 ​
         // 两个玩家轮流打上 X 和 O
         board[0][0] = "X"
         board[2][2] = "O"
         board[1][2] = "X"
         board[1][0] = "O"
         board[0][2] = "X"
 ​
         for i := 0; i < len(board); i++ {
                 fmt.Printf("%s\n", strings.Join(board[i], " "))
         }
 }

向切片追加元素(apped)

append函数:为切片追加新的元素是种常用的操作,内建函数的文档对此函数有详细的介绍。

func append(s []T, vs ...T) []T

第一个参数 s 是一个元素类型为T的切片,其余类型为 T 的值将会追加到该切片的末尾。

append的结果是一个包含原切片所有元素加上新添加元素的切片。

s的底层数组太小,不足以容纳所有给定的值时,它就会分配一个更大的数组。返回的切片会指向这个新分配的数组。

(要了解关于切片的更多内容,请阅读文章 Go 切片:用法和本质。)

 package main
 ​
 import "fmt"
 ​
 func main() {
         var s []int
         printSlice(s)   //输出:len=0 cap=0 []
 ​
         // 添加一个空切片
         s = append(s, 0)
         printSlice(s)   //输出:len=1 cap=1 [0]
 ​
         // 这个切片会按需增长
         s = append(s, 1)
         printSlice(s)   //输出:len=2 cap=2 [0 1]
 ​
         // 可以一次性添加多个元素
         s = append(s, 2, 3, 4)
         printSlice(s)   //输出:len=5 cap=6 [0 1 2 3 4]
 }
 ​
 func printSlice(s []int) {
         fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
 }

Range

for循环的range形式可遍历切片或映射。

当使用 for 循环遍历切片时,每次迭代都会返回两个值。第一个值为当前元素的下标,第二个值为该下标所对应元素的一份副本。

可以将下标或值赋予 _ 来忽略它。

for i, _ := range pow等价于for i := range pow(若需要索引,忽略第二个变量即可)

for _, value := range pow

 package main
 ​
 import "fmt"
 ​
 var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}
 ​
 func main() {
         //range遍历切片pow,i是下标,v是对应值
         for i, v := range pow {
                 fmt.Printf("2**%d = %d\n", i, v)
         }
 ​
         pow := make([]int, 10)
         for i := range pow {
                 pow[i] = 1 << uint(i) // == 2**i
         }
         for _, value := range pow {
         fmt.Print("value = ")
                 fmt.Printf("%d\n", value)
         }
 }

切片练习

 package main
 ​
 import "golang.org/x/tour/pic"
 ​
 func Pic(dx, dy int) [][]uint8 {
   ss := make([][]uint8, dy)
     for y := 0; y < dy; y++ {
       s := make([]uint8, dx)
       for x := 0; x < dx; x++ {
         s[x] = uint8((x + y) / 2)
       }
       ss[y] = s
     }
     return ss
 }
 ​
 func main() {
   pic.Show(Pic)
 }

注意点

  1. 该文件需要导入外部包,需要执行一下命令获取该包

    go get golang.org/x/tour/pic

    若提示go: added golang.org/x/tour v0.1.0则导入成功,会在目录里的go.mod中记录,并生成go.sum文件

  2. go.mod文件用来记录导入包,所以必须要有。若没有需要执行以下命令生成go.mod文件,注意,一个go.mod目录下只能有一个main函数

    go mod init 目录名

  3. 由于pic包没有main包,所以不能用go install

  4. 该文件是base64编码,若想查看需要将IMAGE:更改为data:image/png;base64,,复制到在线解析网站即可观看。或者将编码放在在html<img src="">的src里查看。

映射(map)

哈希表是一种巧妙并且实用的数据结构。它是一个无序的key/value对的集合,其中所有的key都是不同的,然后通过给定的key可以在常数时间复杂度内检索、更新或删除对应的value。

在Go语言中,一个map就是一个哈希表的引用。

map的类型

map[K][V],K和V分别对应key和value。

映射的零值为nilnil映射既没有键,也不能添加键。

map中所有的key都有相同的类型,所有的value也有着相同的类型,但是key和value之间可以是不同的数据类型。

K不要用浮点数类型,因为需要key支持= =比较运算,通过key是否相等判断是否存在。V没有数据类型限制

内置的make函数可以创建一个map,同时还可以指定一些最初的key/value

map中的元素通过key对应的下标语法访问

 package main
 ​
 import "fmt"
 ​
 type Vertex struct {
         Lat, Long float64
 }
 ​
 func main() {
 ​
         //用make函数可以创建一个map
         m := make(map[string]Vertex)
         m["Bell Labs"] = Vertex{
                 40.68433, -74.39967,
         }
         fmt.Println(m["Bell Labs"])
 ​
         //用map字面值的语法创建map,同时还可以指定一些最初的key/value:
         ages := map[string]int{ //创建了一个由string到int的映射(map)
                 "alice":   31,
                 "charlie": 34,
         }
         ages["alice"] = 32
         fmt.Println(ages["alice"]) // "32"
 }

修改映射

  • 在映射m中插入或修改元素:m[key] = elem

  • 获取元素:elem = m[key]

  • 删除元素:delete(m,key)

  • 通过双赋值检测某个key是否存在:elem ,ok = m[key]

    • keym中,oktrue;否则,okfalse
    • key不在映射中,那么elem是该映射元素类型的零值。
    • :若elemok还未声明,可以使用短变量声明:elem ,ok := m[key]
  • 同样的,当从映射中读取或删除某个不存在的key时,结果是映射的元素类型的零值。
 package main
 ​
 import "fmt"
 ​
 func main() {
   m := make(map[string]int)
 ​
   m["Answer"] = 42
   fmt.Println("The value:", m["Answer"])
 ​
   m["Answer"] = 48
   fmt.Println("The value:", m["Answer"])
 ​
   delete(m, "Answer")
   fmt.Println("The value:", m["Answer"])
 ​
   v, ok := m["Answer"]
   fmt.Println("The value:", v, "Present?", ok)
 }