Tour of go

Part1

tour of go 入口

Go程序由包组成

程序入口是main包

1
package main

导入包有两种写法

1
2
3
4
5
6
7
import "fmt"
import "math"

import (
"fmt"
"math"
)

使用分组导入语句(第一种)要更好。

使用导出包需要名字以大写字母开头

例如math.Pi

变量申明

var用于申明变量

变量声明可以包含初始值,如果提供了初始值,则类型可以省略;变量会从初始值中推断出类型。

go申明变量需要把变量类型放在后面 例如

1
2
var a int = 10
a:=10 //:= 结构不能在函数外使用。

在函数中也是类似

1
2
3
4
5
6
7
func add(x int, y int) int {
return x + y
}

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

连续多个已命名形参类型相同,可以省略前面的,只申明最后一个

函数

1
sum := func(a, b int) int { return a+b } (3, 4)

这句话的作用是定义匿名函数并立即调用,最终把结果赋值给 sum

下面写法效果相同:

1
2
3
tmp := func(a, b int) int { return a + b }
sum := tmp(3, 4)
fmt.Println(sum) // 7

函数的返回值数量是任意的

例如

1
2
3
func swap(x, y string) (string, string) {
return y, x
}

函数的返回值甚至可以被命名,会被视作定义在函数顶部的变量

没有参数的 return 语句会直接返回已命名的返回值,也就是「裸」返回值。

1
2
3
4
5
6
7
8
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return
}
func main() {
fmt.Println(split(17))
}//输出 7 10

上述代码中return 返回了x,y

基本类型

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

string

int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr

byte // uint8 的别名

rune // int32 的别名
// 表示一个 Unicode 码位

float32 float64
complex64 complex128

其中intuintuintptr 类型与系统位数相同

零值(未被初始化赋值的变量)

1
2
3
数值   0
bool false
string ""(空字符串)

Go中的类型转换均需要显式转换

1
2
3
4
5
6
7
var i int = 42
var f float64 = float64(i)
var u uint = uint(f)
//或者
i := 42
f := float64(i)
u := uint(f)

新声明一个变量,用已知的另一个变量直接赋值,新变量的类型与其相同

常量

常量的声明需要const关键字

const World = "世界"

for

for是Go语言中唯一的循环结构

与C语言类似,但是没有小括号,且必须加大括号

示例如下

1
2
3
4
sum := 0
for i := 0; i < 10; i++ {
sum += i
}

左右两个;可以省略,省略后也就等于while

1
2
3
4
sum := 1
for sum < 1000 {
sum += sum
}

如果省略循环条件,就会无限循环

1
2
3
4
func main() {
for {
}
}

if

Go中的if判断也是无需小括号,但是要加大括号

1
2
3
if x < 0 {
return sqrt(-x) + "i"
}

可以在if中声明变量,声明的变量可以在else块中使用

1
2
3
4
5
6
7
8
9
func pow(x, n, lim float64) float64 {
if v := math.Pow(x, n); v < lim {
return v
} else {
fmt.Printf("%g >= %g\n", v, lim)
}
// can't use v here, though
return lim
}

switch

Go中的switch只会运行选定的case,运行之后就会break,跳过其他所有的case,如果需要继续往下运行,需要在case的语句末端加上fallthrough

switch的case无需为常量,且取值不限于整数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
fmt.Println("周六是哪天?")
today := time.Now().Weekday()
switch time.Saturday {
case today + 0:
fmt.Println("今天。")
case today + 1:
fmt.Println("明天。")
case today + 2:
fmt.Println("后天。")
default:
fmt.Println("很多天后。")
}
}

switch之后可以不跟条件, 能将一长串 if-then-else 写得更加清晰。

defer

defer 语句会将函数推迟到外层函数返回之后执行。

推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。

推迟调用的函数调用会被压入一个栈中。 当外层函数返回时,被推迟的调用会按照后进先出的顺序调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
fmt.Println("counting")

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

fmt.Println("done")
}
//counting
//done
//9
//8
//...
//0

指针

指针的零值是nil

var p *int

其余操作和C语言类似

1
2
3
4
p := &i         // 指向 i
fmt.Println(*p) // 通过指针读取 i 的值
*p = 21 // 通过指针设置 i 的值
fmt.Println(i) // 查看 i 的值

结构体

一个 结构体(struct)就是一组 字段(field)。

1
2
3
4
type Vertex struct {
X int
Y int
}

访问

指针形式可以使用(*p).X,也可以直接用p.X

1
2
3
4
5
v := Vertex{1, 2}
v.X = 4
//指针形式
p := &v
p.X = 1e9

结构体字面量

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

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

1
2
3
4
5
6
7
8
9
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)//{1 2} &{1 2} {1 0} {0 0}
}

数组

表达式

1
2
var a [10]int
primes := [6]int{2, 3, 5, 7, 11, 13}

数组切片 a[low:high] 左闭右开

1
var s []int = primes[1:4]

切片并不储存任何数据,只是描述底层数组中的一段,因此改变切片数据会改变底层数组元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
names := [4]string{
"John",
"Paul",
"George",
"Ringo",
}
fmt.Println(names)//[John Paul George Ringo]

a := names[0:2]
b := names[1:3]
fmt.Println(a, b)//[John Paul] [Paul George]

b[0] = "XXX"
fmt.Println(a, b)//[John XXX] [XXX George]
fmt.Println(names)//[John XXX George Ringo]
}

切片字面量类似于没有长度的数组字面量。

1
slice := []bool{true, true, false}

等价于

1
2
tmpArray := [3]bool{true, true, false}  // 编译器自动创建的底层数组
slice := tmpArray[:] // 切片引用这个数组

意为创建“一个数组 + 一个引用它的切片”

切片字面量确实会创建一个“底层数组”,但这个数组 不会绑定到一个变量名上,而是由 切片内部自动引用。你无法直接访问这个底层数组,但切片能“看见”它。

切片忽略上下界和python一样

1
2
3
4
5
//var a[10]int
a[0:10]
a[:10]
a[0:]
a[:]

是一个意思

切片具有长度和容量

长度:len(s) 就是它所包含的元素个数。

容量:cap() 是从它的第一个元素开始数,到其底层数组元素末尾的个数。

切片的零值是nil,长度和容量都为0且没有底层数组

1
var s []int

用make创建切片

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

1
2
3
4
a := make([]int, 5)  // len(a)=5
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函数

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

s是一个类型T的切片,其余类型T的会追加到末尾

append的用法一般是

1
s = append(s,2,3,4)

append会拓展数组

range遍历

for的range形式用以遍历切片或map

1
2
3
4
5
6
7
var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}

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

range遍历每次都会返回两个值

i,v分别为下标和对应的值,如果要省略其中一个就把要省略的的写成_,只需索引可以忽略第二个变量

map映射

将键映射到值,零值为nil,没有键也不能添加键

需要用make进行初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
type Vertex struct {
Lat, Long float64
}

var m map[string]Vertex//此时的 m 是 nil map,不能直接写入。

func main() {
m = make(map[string]Vertex)//使用 make 来真正创建一个空 map,map 的底层哈希表在这里被分配出来。 现在 m 可以安全存储数据。
m["Bell Labs"] = Vertex{
40.68433, -74.39967,
}//结构体字面量可以只写值,不写字段名,按顺序赋值。
fmt.Println(m["Bell Labs"])
}
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,
},
}
func main() {
fmt.Println(m)
}

make可以加一个可选参数指定Map的初始容量

若顶层类型只是一个类型名,那么你可以在字面量的元素中省略它。

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

函数

函数值可以作为其他函数的参数/返回值

函数可以是一个闭包

1
2
3
4
5
6
7
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}

sum 是在 adder 函数内部定义的变量

adder 返回一个匿名函数 func(x int) int

这个匿名函数引用了外部变量 sums

即使 adder 执行结束,sum 仍然存在

每个通过 adder() 创建出来的闭包都有自己独立的 sum

方法

Go语言没有类,可以用方法来定义类

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

func (v Vertex) Abs() 中,v是Abs方法的名字,type为Vertex的receiver

带有receiver参数的函数在Go中会被视为方法

func (v *Vertex) Scale(f float64)

加上*即为指针类型的方法,在此例中可以直接对原始 Vertex的值进行操作

方法与指针重定向

一个函数,对他的指针参数传递不同类型的值会报错,对其他类型的参数传递指针类型的值也会报错

但对于方法而言,如果一个接收者为指针的方法被调用,接收者既可以是指针也可以是非指针,反之亦然。Go语言会自动将其在指针/非指针之间解释

这样可以使方法调用更自然,使用者不需要关心接收者是否为指针

例如

1
2
3
4
5
6
7
func (v *Vertex) Scale(f float64) {
v.X *= f
v.Y *= f
}
p := &v
p.Scale(10) // OK
v.Scale(5) //OK

Go会自动将最后一段转化为(&v).Scale(5)

接口

1
2
3
4
type 接口名 interface {
方法名1(参数列表) 返回值列表
方法名2(参数列表) 返回值列表
}
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
41
42
package main

import (
"fmt"
"math"
)

type Abser interface {
Abs() float64
}

func main() {
var a Abser
f := MyFloat(-math.Sqrt2)
v := Vertex{3, 4}

a = f // a MyFloat 实现了 Abser
a = &v // a *Vertex 实现了 Abser

// 下面一行,v 是一个 Vertex(而不是 *Vertex)
// 所以没有实现 Abser。
a = v

fmt.Println(a.Abs())
}

type MyFloat float64

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

type Vertex struct {
X, Y float64
}

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

接口被定义为一组方法签名

接口无需显式声明,没有”implements”

一个类型通过实现一个接口的所有方法来实现这个接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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)

调用方法时,接口值会执行与其底层type类型相同的同名方法

nil接口

nil接口值不保存值和类型,因此调用时会报错

但如果接口内的具体值为nil,方法依然会被调用,且不会触发空指针异常(前提是这个接口自身不为nil)

空接口

空接口可以保存任意类型的值

1
2
3
4
var i interface{}
func describe(i interface{}) {
fmt.Printf("(%v, %T)\n", i, i)
}

类型断言

1
t := i.(T)//如果i并未保存T类型的值,会触发panic

这条语句断言i保存了类型T,并且把T赋值给t

判断语句

1
t, ok := i.(T)

如果i保存了一个T,会把T赋值给t,ok为true

否则t为T的零值,ok为false