【Go学习记录】入门

起步基础先跟着Go语言之旅
基础学习结构:

  • 包、变量、函数
  • 流程控制语句
  • struct、slice和映射
  • 方法和接口
  • 并发

包、变量、函数

  • 程序从 main 包开始运行。包名与导入路径的最后一个元素一致。例如,“math/rand” 包中的源码均以 package rand
  • 尽量分组导入
  • 一个名字以大写字母开头,那么它就是已导出的。Pizza 就是个已导出名,Pi 也同样,它导出自 math 包。pizza 和 pi 并未以大写字母开头,所以它们是未导出的。在导入一个包时,你只能引用其中已导出的名字。任何 “未导出”的名字在该包外均无法访问
  • 当连续两个或多个函数的已命名形参类型相同时,除最后一个类型以外,其它都可以省略。如下x int,y int缩写
    1
    2
    3
    4
    5
    6
    7
    8
    9
    package main

    import "fmt"

    func add(x, y int) int {
    return x + y
    }


  • Go 的返回值可被命名,它们会被视作定义在函数顶部的变量。如下返回了sum
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    package main

    import "fmt"

    func split(sum int) (x, y int) {
    x = sum * 4 / 9
    y = sum - x
    return
    }

    func main() {
    fmt.Println(split(17))
    }

  • var 语句用于声明一个变量(列表),跟函数的参数列表一样,类型在最后
  • :=可在类型明确的地方代替 var
  • Go 在不同类型的项之间赋值时需要显式转换
  • 常量的声明与变量类似,使用 const关键字,不能用 :=语法声明

流程控制语句

  • for循环:Go 的 for 语句后面的三个构成部分外没有小括号
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    package main
    import "fmt"
    func main(){
    sum:=0
    for i=0;i<10;i++{
    sum+=i
    }

    fmt.Println(sum)
    }
  • 同样if语句表达式也不需要小括号,同 for 一样, if 语句可以在条件表达式前执行一个简单的语句(当然作用域只在if大括号内)。
    1
    2
    3
    4
    5
    6
    7
    func pow(x, n, lim float64) float64 {
    if v := math.Pow(x, n); v < lim {
    return v
    }
    return lim
    }

  • defer语句:defer 语句会将函数推迟到外层函数返回之后执行(推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用),推迟的函数调用会被压入一个栈(先进后出)中
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    package main

    import "fmt"

    func main() {
    fmt.Println("counting")

    for i := 0; i < 10; i++ {
    defer fmt.Println(i)
    }

    fmt.Println("done")
    }

    运行结果为
    counting
    done
    9
    8
    7
    6
    5
    4
    3
    2
    1
    0

struct、slice和映射

  • 指针:Go的指针保存了值的内存地址。与 C 不同,Go 没有指针运算,其余类似
  • 结构体指针:有一个指向结构体的指针 p,可通过 (*p).X来访问其字段 X。也可以隐式间接引用,直接写 p.X
  • 结构体文法:直接列出字段的值来新分配一个结构体。使用 Name: 语法可以仅列出部分字段。(字段名的顺序无关。)特殊的前缀 & 返回一个指向结构体的指针。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    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)
    }

  • 数组:var a [10]int

    切片(slice)

  • 切片:GO语言中切片比数组使用更频繁。与Python切片类似,a[low : high]为左闭右开。更改切片的元素会修改其底层数组中对应的元素,与它共享底层数组的切片都会观测到这些修改。
  • 切片文法:这是一个数组的文法[3]bool{true, true, false}
    现构建一个引用了它的切片:[]bool{true, true, false}
  • 切片长度和容量:切片长度是指自身包含的元素个数,用len()获取;切片容量指的是切片的第一个元素开始数,到其底层数组元素末尾的个数,用cap()获取
  • nil切片:切片的零值是 nil。nil 切片的长度和容量为 0 且没有底层数组。
  • 用 make 创建切片:切片可以用内建函数 make 来创建,也是创建动态数组的方式。make 函数会分配一个元素为零值的数组并返回一个引用了它的切片:a := make([]int, 5) // len(a)=5要指定它的容量,需向 make传入第三个参数:b := make([]int, 0, 5) /* len(b)=0, cap(b)=5*/ b = b[:cap(b)] /*len(b)=5, cap(b)=5 */ b = b[1:] /* len(b)=4, cap(b)=4 */
  • 切片可嵌套
  • append():为切片追加新的元素func append(s []T, vs ...T) []T append的第一个参数 s是一个元素类型为 T 的切片,其余类型为 T 的值将会追加到该切片的末尾。当 s 的底层数组太小,不足以容纳所有给定的值时,它就会分配一个更大的数组。返回的切片会指向这个新分配的数组。如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    package main

    import "fmt"

    func main() {
    var s []int
    printSlice(s)

    // 添加一个空切片
    s = append(s, 0)
    printSlice(s)

    // 这个切片会按需增长
    s = append(s, 1)
    printSlice(s)

    // 可以一次性添加多个元素
    s = append(s, 2, 3, 4)
    printSlice(s)
    }

    func printSlice(s []int) {
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
    }

  • Range:直接上code(切片这块基本和Python使用相同)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}
    //第一个值(i)为当前元素的下标,第二个值(v)为该下标所对应元素的一份副本。
    func main() {
    for i, v := range pow {
    fmt.Printf("2**%d = %d\n", i, v)
    }
    }

    func main() {
    pow := make([]int, 10)
    for i := range pow {
    pow[i] = 1 << uint(i) // == 2**i
    }
    for _, value := range pow {
    fmt.Printf("%d\n", value)
    }
    }


映射(map)

映射将键映射到值。映射的零值为 nil 。nil 映射既没有键,也不能添加键。make 函数会返回给定类型的映射,并将其初始化备用。

映射文法与结构体类似,但要有键名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14

type Vertex struct {
Lat, Long float64
}

var m = map[string]Vertex{
"Bell Labs": Vertex{
40.68433, -74.39967,
},
"Google": Vertex{
37.42202, -122.08408,
},
}


修改映射:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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)
}

函数

函数的包:Go 函数可以是一个闭包。闭包是一个函数值,它引用了其函数体之外的变量。该函数可以访问并赋予其引用的变量的值,换句话说,该函数被这些变量“绑定”在一起。

方法和接口

方法是比较简单的,实际上跟类是一个应用(有方法接受对象的函数);接口有点难。

方法

Go没有类:但可以为结构体类型定义方法。方法就是一类带特殊的 接收者参数的函数。方法接收者在它自己的参数列表内,位于 func 关键字和方法名之间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

type Vertex struct{
X, Y float64
}

func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
v := Vertex{3, 4}
fmt.Println(v.Abs())
}
//也可为非结构体类型声明方法(只能在同一包内定义的类型的接收者声明方法,而不能为其它包内定义的类型(包括 int 之类的内建类型)的接收者声明方法。)
type MyFloat float64

func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}


为指针接收者声明方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type Vertex struct {
X, Y float64
}

func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}

func main() {
v := Vertex{3, 4}
v.Scale(10)
fmt.Println(v.Abs())
}


由于方法经常需要修改它的接收者,指针接收者值接收者更常用。若使用值接收者,那么 Scale 方法会对原始 Vertex 值的副本进行操作。Scale 方法必须用指针接受者来更改main 函数中声明的 Vertex 的值

针对指针的函数与方法:

  • 带指针参数的函数必须接受一个指针
    var v Vertex ScaleFunc(v, 5) // 编译错误!ScaleFunc(&v, 5) // OK
  • 指针为接收者的方法被调用时,接收者既能为值又能为指针
    var v Vertexv.Scale(5) // OK p := &v p.Scale(10) // OK
    所以一般情况下,Go 会将语句 v.Scale(5)解释为 (&v).Scale(5)

所以选择指针为方法接收者的原因有这些:

  • 方法能够直接修改其接收者指向的值
  • 避免在每次调用方法时复制该值。若值的类型为大型结构体时,这样做会更加高效

接口

接口类型是由一组方法签名定义的集合
接口与隐式实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

type I interface {
M()
}

type T struct {
S string
}

// 此方法表示类型 T 实现了接口 I,但我们无需显式声明此事。
func (t T) M() {
fmt.Println(t.S)
}

func main() {
var i I = T{"hello"}
i.M()
}

接口值:可以像其它值一样传递,可用作函数的参数或返回值。接口值可以看做包含值和具体类型的元组:(value, type)接口值调用方法时会执行其底层类型的同名方法。(如上例中i调用M()方法执行的是对T的方法)
底层为nil接口值:即便接口内的具体值为 nil,方法仍然会被 nil 接收者调用。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
type I interface {
M()
}

type T struct {
S string
}

func (t *T) M() {
if t == nil {
fmt.Println("<nil>")
return
}
fmt.Println(t.S)
}

func main() {
var i I

var t *T
i = t
describe(i)
i.M()

i = &T{"hello"}
describe(i)
i.M()
}


然而nil 接口值既不保存值也不保存具体类型。为 nil 接口调用方法会产生运行时错误。如下:
1
2
3
4
5
6
7
8
9
10
type I interface {
M()
}

func main() {
var i I
describe(i)
i.M()
}
//i接口的元组内并未包含能够指明该调用哪个 具体 方法的类型

空接口:指定了零个方法的接口值interface{}空接口可保存任何类型的值。(因为每个类型都至少实现了零个方法。)空接口被用来处理未知类型的值。常见使用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
var i interface{}
describe(i)

i = 42
describe(i)

i = "hello"
describe(i)
}

func describe(i interface{}) {
fmt.Printf("(%v, %T)\n", i, i)
}


类型断言:提供了访问接口值底层具体值的方式;判断一个接口值是否保存了一个特定的类型,类型断言可返回两个值:其底层值以及一个报告断言是否成功的布尔值t, ok := i.(T)(若 i 保存了一个 T,那么 t 将会是其底层值,而 ok 为 true;否则,ok为false而t为T类型的零值)。类型断言语法和读取一个映射时有相同之处

类型选择:是一种按顺序从几个类型断言中选择分支的结构。类型选择与一般的 switch 语句相似,不过类型选择中的 case 为类型(而非值), 它们针对给定接口值所存储的值的类型进行比较。其声明与类型断言 i.(T)的语法相同,T被替换成了关键字 type

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

func do(i interface{}) {
switch v := i.(type) {
case int:
fmt.Printf("Twice %v is %v\n", v, v*2)
case string:
fmt.Printf("%q is %v bytes long\n", v, len(v))
default:
fmt.Printf("I don't know about type %T!\n", v)
}
}

func main() {
do(21)
do("hello")
do(true)
}


三个常用接口(包):

  • Stringer
  • reader
  • image

Stringer:fmt 包中定义的 Stringer 是最普遍的接口之一。它可以用字符串描述自己的类型。fmt 包(还有很多包)都通过此接口来打印值。

1
2
3
4
type Stringer interface {
String() string
}

Reader:io包指定了 io.Reader接口,它表示从数据流的末尾进行读取。io.Reader接口有一个 Read 方法:func (T) Read(b []byte) (n int, err error);Read 用数据填充给定的字节切片并返回填充的字节数和错误值。在遇到数据流的结尾时,它会返回一个 io.EOF 错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"fmt"
"io"
"strings"
)

func main() {
r := strings.NewReader("Hello, Reader!")

b := make([]byte, 8)
for {
n, err := r.Read(b)
fmt.Printf("n = %v err = %v b = %v\n", n, err, b)
fmt.Printf("b[:n] = %q\n", b[:n])
if err == io.EOF {
break
}
}
}


图像:image 包定义了 Image 接口:

1
2
3
4
5
6
7
8
9
package image

type Image interface {
ColorModel() color.Model
Bounds() Rectangle
At(x, y int) color.Color
}


并发

goroutine

Go程(goroutine):是由 Go 运行时管理的轻量级线程。go f(x, y, z)会启动一个新的 Go 程并执行。f(x, y, z)f, x , yz的求值发生在当前的 Go 程中,而 f 的执行发生在新的 Go 程中。

Go 程在相同的地址空间中运行,因此在访问共享的内存时必须进行同步,sync包提供了这种能力.

信道

信道:带有类型的管道,通过它用信道操作符<- 来发送或者接收值。
ch <- v // 将 v 发送至信道 ch v := <-ch // 从 ch 接收值并赋予 v(“箭头”就是数据流的方向)

信道在使用前必须创建:ch := make(chan int)默认情况下,发送和接收操作在另一端准备好之前都会阻塞。这使得 Go 程可以在没有显式的锁或竞态变量的情况下进行同步。没懂…..以下为gpt的形象解释

想象一下你正在组织一场接力赛。在这个接力赛中,每个参赛者(代表一个Goroutine)需要等待前一个队友完成他的部分并传递接力棒(代表信道中的数据)给他,然后他才能开始跑。如果前一个队友还没有跑到(即没有数据发送到信道),下一个队友(接收操作)就必须等待。反之,如果一个队友到达并准备传递接力棒,但下一个队友还没有准备好接手,他也需要等待。这个过程确保了所有的参赛者按照正确的顺序开始和完成他们的部分,没有人会跑得太早或太晚,从而无需额外的指示(即无需显式的锁)就自然而然地实现了同步。

通过使用信道的这种阻塞机制,Go能够在并发程序中简化数据的同步和通信,避免了直接使用锁或处理竞态变量的复杂性。这使得编写并发程序变得更加直观和安全。

具体例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "fmt"

func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // 将和送入 c
}

func main() {
s := []int{7, 2, 8, -9, 4, 0}

c := make(chan int)
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c // 从 c 中接收

fmt.Println(x, y, x+y)
}


带缓冲的信道:将缓冲长度作为第二个参数提供给 make 来初始化一个带缓冲的信道,ch := make(chan int, 100);仅当信道的缓冲区填满后,向其发送数据时才会阻塞。当缓冲区为空时,接受方会阻塞

range和close

发送者可通过close关闭一个信道来表示没有需要发送的值了。接收者可以通过为接收表达式分配第二个参数来测试信道是否被关闭:若没有值可以接收且信道已被关闭,那么在执行完v, ok := <-ch之后ok会被设置为 false。

循环for i := range c会不断从信道接收值,直到它被关闭。

WARNING: 只有发送者才能关闭信道,而接收者不能。向一个已经关闭的信道发送数据会引发程序恐慌(panic)。

WARNING: 信道与文件不同,通常情况下无需关闭它们。只有在必须告诉接收者不再有需要发送的值时才有必要关闭,例如终止一个 range 循环。以下为示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"fmt"
)

func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x+y
}
close(c)
}

func main() {
c := make(chan int, 10)
go fibonacci(cap(c), c)
for i := range c {
fmt.Println(i)
}
}

select语句

select语句使一个 Go 程可以等待多个通信操作。

select会阻塞到某个分支可以继续执行为止,这时就会执行该分支。当多个分支都准备好时会随机选择一个执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
}

func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}


没懂…

默认选择:当 select 中的其它分支都没有准备好时,default 分支就会执行。为了在尝试发送或者接收时不发生阻塞,可使用 default 分支:

1
2
3
4
5
6
select {
case i := <-c:
// 使用 i
default:
// 从 c 中接收会阻塞时执行
}

sync.Mutex

显而易见信道非常适合在各个 Go 程间进行通信。

但是如果我们并不需要通信呢?如果只是想保证每次只有一个 Go 程能够访问一个共享的变量,从而避免冲突?

这里涉及的概念叫做 互斥(mutual*exclusion) ,我们通常使用 互斥锁(Mutex) 这一数据结构来提供这种机制。

Go 标准库中提供了 sync.Mutex互斥锁类型及其两个方法:

  • Lock
  • Unlock
    我们可以通过在代码前调用Lock方法,在代码后调用Unlock方法来保证一段代码的互斥执行。参见 Inc方法。

我们也可以用 defer 语句来保证互斥锁一定会被解锁。(参考Value方法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package main

import (
"fmt"
"sync"
"time"
)

// SafeCounter 的并发使用是安全的。
type SafeCounter struct {
v map[string]int
mux sync.Mutex
}

// Inc 增加给定 key 的计数器的值。
func (c *SafeCounter) Inc(key string) {
c.mux.Lock()
// Lock 之后同一时刻只有一个 goroutine 能访问 c.v
c.v[key]++
c.mux.Unlock()
}

// Value 返回给定 key 的计数器的当前值。
func (c *SafeCounter) Value(key string) int {
c.mux.Lock()
// Lock 之后同一时刻只有一个 goroutine 能访问 c.v
defer c.mux.Unlock()
return c.v[key]
}

func main() {
c := SafeCounter{v: make(map[string]int)}
for i := 0; i < 1000; i++ {
go c.Inc("somekey")
}

time.Sleep(time.Second)
fmt.Println(c.Value("somekey"))
}


芜湖…..整理完了,(应该是copy完了);并发这块还不是很懂,其他还比较简单。
主要参考:
GO语言之旅