go语言学习

学习指南

与C语言类型系统的比较

变量类型的位置

https://blog.go-zh.org/gos-declaration-syntax

支持的类型

如下是golang支持的所有的内置类型

bool
string
int  int8  int16  int32  int64
uint uint8 uint16 uint32 uint64 uintptr
byte // alias for uint8
rune // alias for int32
    // represents a Unicode code point
float32 float64
complex64 complex128

控制语句

for

1
2
3
4
5
6
7
8
9
10
11
for i := 0; i < 10; i++ {
sum += i
}

for ; sum < 1000; {
sum += sum
}

for sum < 1000 {
sum += sum
}

if

1
2
3
4
if v := math.Pow(x, n); v < lim {
return v
}
return lim

switch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
fmt.Print("Go runs on ")
switch os := runtime.GOOS; os {


case "linux":
fmt.Println("Linux.")
fallthrough
case "darwin":
fmt.Println("OS X.")
default:
// freebsd, openbsd,
// plan9, windows...
fmt.Printf("%s.\n", os)
}
}

defer

defer这个关键词原本适用于从panicrecover,但是现在变成了一种语句用来释放某些资源例如defer mu.Unlock()

  1. defer后的函数参数在使用defer的时候就已经被求值了
1
2
3
4
5
6
func a() {
i := 0
defer fmt.Println(i)
i++
return
}
  1. defer调用使用LIFO的顺序
1
2
3
4
5
6
7
8
9
10
11
12
13
func b() {
for i := 0; i < 4; i++ {
defer fmt.Print(i)
}
}
```

3. defer后的函数能读取并且赋值给函数返回值
``` go
func c() (i int) {
defer func() { i++ }()
return 1
}

切片

博客原文地址:https://blog.golang.org/go-slices-usage-and-internals

切片后和原数组和原来数组共用同一个底层内容,长度是该底层数组的长度,每一个切片实际保存三个内容(如图),这也被称作数组段的描述符:
slice

  • 指向最开始元素的指针
  • 切片长度
  • 切片的capacity

其中capacity是从数组该切片的第一个元素开始到所引用的底层数组最后一个元素的长度,所以当切片移动了头部的元素,其capacity也会缩减相应的大小。
slice2

切片的语法很像数组的语法,即取消掉数组的[]中的值:

letters := []string{"a", "b", "c", "d"}

并且也能被内置的函数make来创建:
make 签名式

func make([]T, len, cap) []T

切片的扩容可以新生成一个更大capacity的切片,将原来的切片内容通过内置函数copy复制进去

copy 签名式

func copy(dst, src []T) int

扩容大小

t := make([]byte, len(s), (cap(s)+1)*2)
copy(t, s)
s = t

最终效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
func AppendByte(slice []byte, data ...byte) []byte {
m := len(slice)
n := m + len(data)
if n > cap(slice) { // if necessary, reallocate
// allocate double what's needed, for future growth.
newSlice := make([]byte, (n+1)*2)
copy(newSlice, slice)
slice = newSlice
}
slice = slice[0:n]
copy(slice[m:n], data)
return slice
}

内置的append 签名式

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

append 独立的元素

1
2
3
4
a := make([]int, 1)
// a == []int{0}
a = append(a, 1, 2, 3)
// a == []int{0, 1, 2, 3}

append 两个分片

1
2
3
4
a := []string{"John", "Paul"}
b := []string{"George", "Ringo", "Pete"}
a = append(a, b...) // equivalent to "append(a, b[0], b[1], b[2])"
// a == []string{"John", "Paul", "George", "Ringo", "Pete"}

重新分片不会对底层数组进行拷贝,整个数组将会一直保存在内存中,直到不再被引用。

1
2
3
4
5
6
var digitRegexp = regexp.MustCompile("[0-9]+")

func FindDigits(filename string) []byte {
b, _ := ioutil.ReadFile(filename)
return digitRegexp.Find(b)
}

从这个函数中返回的字节流匹配分片后,仍旧引用该大数据流,但是有用的字节只占所有内容的少数,更加好的做法是:

1
2
3
4
5
6
7
func CopyDigits(filename string) []byte {
b, _ := ioutil.ReadFile(filename)
b = digitRegexp.Find(b)
c := make([]byte, len(b))
copy(c, b)
return c
}

range

1
2
3
for i, v := range pow {
fmt.Printf("2**%d = %d\n", i, v)
}

如果对脚标i的值不感兴趣,可以使用_代替i。

映射

1
2
3
4
5
6
7
8
9
10
11
12
var m = map[string]Vertex{
"Bell Labs": Vertex{
40.68433, -74.39967,
},
"Google": Vertex{
37.42202, -122.08408,
},
}

func main() {
fmt.Println(m)
}

判断映射对应的值是否存在

1
2
v, ok := m["Answer"]
fmt.Println("The value:", v, "Present?", ok)

添加新的映射只需要为该键进行赋值就行了。

函数闭包

闭包是一个函数值,函数可以访问并且修改函数体外面的值。adder()返回一个分别绑定在sum值上面的闭包,每次迭代,这三个闭包都对三个独立的sum值进行运算,并且加上不同的值。

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

func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}

func main() {
pos, neg, new := adder(), adder(), adder()
for i := 0; i < 10; i++ {
fmt.Println(
pos(i),
neg(-2*i),
new(2*i),
)
}
}

然后试着写的巨复杂的fibonacci数列

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
package main

import "fmt"

var one = 0
var two = 1

// fibonacci is a function that returns
// a function that returns an int.
func fibonacci() func() int {
cur := 0
p := 0
pp := 0
return func() int {
if cur == 0 {
cur++
return one
} else if (cur == 1) {
pp = 0;
p = 1;
cur++
return two
}
defer func() {
pp, p = p, p+pp
}()
return pp + p
}
}

func main() {
f := fibonacci()
for i := 0; i < 10; i++ {
fmt.Println(f())
}
}

方法

Go中没有类,但是可以为结构体写方法,将方法的接受者置于func和方法名字之间,方法只是一个带有接受者参数的函数。

1
2
3
4
5
6
7
8

type Vertex struct {
X, Y float64
}

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

另外,接受者分为值接受者指针接受者,值接受者是一份拷贝,无法修改其参数的值,但是指针可以。

1
2
3
4
5
6
7
8

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
}

接口

接口类型是有一组方法的签名,其变量保存实现了这些方法的值。

接口的声明:

1
2
3
type Abser interface {
Abs() float64
}

接口值保存一个具体类型的具体值,接口方法调用时,执行其底层类型对应的同名方法。

若某个结构体为:

1
2
3
4
type T struct {
S string
dd int
}

那么将这个赋予给某个接口,其接口的底层为:

(&{Hello 0}, *main.T)

接口类型的值不能为空,必须引用类型的具体实现,但是其内部值能为空。除此之外,空接口可以用来接受任何接口类型,写法为:

var i interface{}

类型断言

t := i.(T)\

该语句断言接口值 i 保存了具体类型 T,并将其底层类型为 T 的值赋予变量 t,并且如果未通过断言则引发一个panic。
和映射一样,可以使用一个bool值来判断是否通过断言,不同的是,map没有使用该bool值的时候并不会引发panic,而断言必须使用该bool值才不会引发panic。

t, ok := i.(T)

类型选择

将具体类型T替换为了关键词type,使用switch语句进行分支选择:

1
2
3
4
5
6
7
8
switch v := i.(type) {
case T:
// v 的类型为 T
case S:
// v 的类型为 S
default:
// 没有匹配,v 与 i 的类型相同
}

接口小练习:

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

package main

import "fmt"

type IPAddr [4]byte

// TODO: Add a "String() string" method to IPAddr.
func (ip IPAddr) String() string {
return fmt.Sprintf("%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3])
}


func main() {
hosts := map[string]IPAddr{
"loopback": {127, 0, 0, 1},
"googleDNS": {8, 8, 8, 8},
}
for name, ip := range hosts {
fmt.Printf("%v: %v\n", name, ip)
}
}

错误处理

提供了一个接口error

1
2
3
type error interface {
Error()
}

相应的类型需要为其进行"implement"

1
2
3
4
5
6
7
8
9
10
11
func (e ErrNegativeSqrt) Error() string {
return fmt.Sprintf("cannot Sqrt negative number: %v", float64(e))
}

func Sqrt(x float64) (float64, error) {
if x > 0 {
return x, nil
} else {
return x, ErrNegativeSqrt(-2)
}
}

goroutine

运行一个gorutine

go f(x, y, z)

会启动一个新的 Go 程并执行 f(x, y, z)

信道

1
2
ch <- v    // 将 v 发送至信道 ch。
v := <-ch // 从 ch 接收值并赋予 v。

并且需要注意,信道使用之前也必须创建。

ch := make(chan int)

带缓冲信道

ch := make(chan int, 100)

只有在缓冲区填满的时候,发送端才会被阻塞,接收方在信道内内容为0时候进行阻塞。

range和close

发送方可以关闭信道,但是接收方不行。利用:

v, ok := <-ch

ok的值可以判断信道中是否还有内容,利用for i := range c可以不断从信道中读取内容,直到信道被关闭为止,如果没有关闭信道,则会产生死锁。

deadlock

select 多路选择

1
2
3
4
5
6
7
8
9
10
11
12
13
14
tick := time.Tick(100 * time.Millisecond)	// channal
boom := time.After(500 * time.Millisecond) //channal
for {
select {
case <-tick:
fmt.Println("tick.")
case <-boom:
fmt.Println("BOOM!")
return
default:
fmt.Println(" .")
time.Sleep(50 * time.Millisecond)
}
}

互斥锁(mutex)

1
2
3
4
5
6
7
8
9
10
11
12
13
// 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()
}

当然也可以用defer来进行解锁

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

FAQ

未使用的variable/import

未使用的变量可能导致一个bug,而且未使用的import会降低编译速度,Go选择拒绝编译,当有未使用的variable/import存在时。
当然可以在debug的时候选择使用 _ 标记:

1
2
var astring string // unused
_ = astring

有时候需要某个package中的某个api进行调试,但是最后的完成的版本并不需要该package,可以选择使用如下的方法处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
import (
"fmt"
_ "syscall"
)
func printAll(a ... int) {
for i, v := range a {
fmt.Println(i, v)
}

}
func main() {
printAll(10, 20, 30 , 123, 231, 2, 1)
}