tour of go day2

感觉光看tour of go的话3天就能全看完了

向切片批量添加元素

func append(s []T, vs ...T) []T
append的第一个参数是源切片,然后后面可以跟任意个参数作为追加元素,最后返回拼接完成的切片
一般用法如下s = append(s,1,2,3)
这个append会动态拓展数组容量

for range

for range形式的循环可以用来方便的遍历切片或者Map
每次迭代的参数固定为两个值,第一个是当前下标,第二个是当前下标对应的值,如果不写第二个参数就只返回下标

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

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

Map

Map用于处理键值对间的映射关系,定义方法为var m map[keyType]valueType
默认未初始化的Mapnil,需要用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

func main() {
m = make(map[string]Vertex)
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
15
16
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的初始容量
与创建结构体时必须在{}前加类型名不同,初始化Map时可以省略结构体类型名

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

Map支持直接通过下标访问元素
m[key] = elem
elem = m[key]
使用delete删除元素
delete(m, key)
查询元素会返回两个值,分别是查询结果和元素是否存在,如果不存在的话则返回对应的零值和false
elem, ok = m[key]

函数值(类似函数指针)

函数可以作为值传递,函数类型定义为func(argT...) retT

1
2
3
4
5
6
7
8
9
10
11
12
13
func compute(fn func(float64, float64) float64) float64 {
return fn(3, 4)
}

func main() {
hypot := func(x, y float64) float64 {
return math.Sqrt(x*x + y*y)
}
fmt.Println(hypot(5, 12))

fmt.Println(compute(hypot))
fmt.Println(compute(math.Pow))
}

函数闭包

go的函数可以作为一个闭包维持函数体外的变量的生命周期,简单地说就是一个函数作为函数指针函数对象储存时,其引用的函数外的变量不会被回收,而是由这个函数维持其生命周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func adder() func(int) int {
sum := 0 // 在adder结束后sum并不会被回收
return func(x int) int {
sum += x
return sum
}
}

func main() {
pos, neg := adder(), adder() // pos,neg各拥有一个独立的sum
for i := 0; i < 10; i++ {
fmt.Println(
pos(i), // 0,1,3,6....
neg(-2*i), // 0,-2,-6,-12...
)
}
}

为类型定义方法

go中没有class的概念,但是可以为类型添加方法,然后就可以用其他语言类似的方式调用某个类型的方法

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())
}

注意到这里在函数名前多了(v Vertex),go tour 中称其为receiver参数,只要带这个参数的函数都会被认为是对应类型的方法,这里的v在函数中有点类似其他语言中的thisself,但注意到这里使用的是深拷贝,调用这种类型的方法并不会修改对象的值

如果将接收者参数改为(v *Vertex),这时v是方法所属对象的指针,现在对v做的操作就会修改调用改方法的对象的值了

方法与指针重定向

对于函数而言,如果参数定义为指针,向其传非指针的操作会导致静态检查不通过,反之亦然
但对于方法而言,一个定义接收者为指针的方法可以由指针或非指针调用,反之亦然,这是因为对于指针接收者方法而言,如果传递的不是指针,go会自动将其转换为指针v.method() -> (&v).method (这符合开发时对指针接收者方法的预期,即应该修改调用者的值),而对于非指针方法,同样会自动进行解引用操作

当然多数情况下最好使用指针接收者,转递指针的效率要比拷贝整个结构体要高得多

接口

接口是一组方法签名,可以作为类型使用,任何实现了接口所要求的所有方法的变量都可以被这个接口类型持有(也就是虚函数那一套)

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
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
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
type I interface {
M()
}

type T struct {
S string
}

func (t *T) M() {
fmt.Println(t.S)
}

type F float64

func (f F) M() {
fmt.Println(f)
}

func main() {
var i I

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

i = F(math.Pi)
describe(i)
i.M()
}

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

nil接口

接口的对应的值可能为nil,即使如此接口仍可以调用其对应值的方法(因为有类型信息),此时需要在对应方法里手动处理nil的情况,注意到接口本身并不为nil
nil接口是真的什么信息都没有,此时调用方法就会导致空指针错误

零接口

接口可以什么方法都不定义,此时它可以接受任何类型,也就是类似void*

类型断言

接口可以通过直接赋值来读取值,也可以通过类型断言读取值
t := i.(T)
该语句断言接口变量i保存了类型为T的值并赋值给t,此时如果保存的变量类型和T不同则会直接panic
t, ok := i.(T)
如果加上ok参数,则在类型不同时会返回T对应的零值和false,并不会抛panic

类型选择

类型选择用于处理接口可能存在多种类型的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func do(i interface{}) {
switch v := i.(type) {
case int:
fmt.Printf("二倍的 %v 是 %v\n", v, v*2)
case string:
fmt.Printf("%q 长度为 %v 字节\n", v, len(v))
default:
fmt.Printf("我不知道类型 %T!\n", v)
}
}

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

通过将具体的类型名换成关键字type就可以获取接口底层值得类型,然后就可以写case了,注意到这个写法只能在type swtich中存在

Stringer

通过实现String()方法使用Println打印自定义的类型描述

1
2
3
type Stringer interface {
String() string
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Person struct {
Name string
Age int
}

func (p Person) String() string {
return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
}

func main() {
a := Person{"Arthur Dent", 42}
z := Person{"Zaphod Beeblebrox", 9001}
fmt.Println(a, z) // Arthur Dent (42 years) Zaphod Beeblebrox (9001 years)
}

Error

通过实现Error方法可以定义自己的错误输出

1
2
3
type error interface {
Error() string
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type ErrNegativeSqrt float64

func (e ErrNegativeSqrt) Error() string{
return fmt.Sprintf("cannot Sqrt negative number: %f",float64(e))
}

func Sqrt(x float64) (float64, error) {
if x<0{
return 0,ErrNegativeSqrt(x)
}
return 0, nil
}

func main() {
fmt.Println(Sqrt(2))
fmt.Println(Sqrt(-2))
}

这里在sqrt的参数为负数时就返回ErrNegativeSqrt
注意到fmt.Sprint在接受一个实现了error接口的值时会尝试调用其Error方法,所以这里不能直接调用fmt.Sprint,至少要先做一下类型转换,不然会陷入Error -> Sprint -> Error...的循环

从命令行接受参数

使用os包,os.Args是一个包含了所有命令行参数的切片,os.Args[0]为程序路径

Author

SGSG

Posted on

2025-07-28

Updated on

2025-07-28

Licensed under