这里我使用的是大佬提供的Docker镜像,直接拉取即可使用,不需要配置Go环境。

1
2
3
4
5
6
7
#学习地址 https://hunterhug.github.io/goa.c/#/golang/README

# 拉镜像
docker pull hunterhug/gotourzh

# 后台运行
docker run -d -p 9999:9999 hunterhug/gotourzh
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
//未导出的函数是小写,导出的函数是大写
![](20240824153903.png)

package main

import "fmt"

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

func main() {
    fmt.Println(add(89,64))
}

x int , y int ==> x,y int

多值返回

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

命名返回值

1
2
3
4
5
6
7
8
9
func split(sum int) (x,y int) {
    x = sum * 4 / 9
    y = sum - x
    return
}

func main() {
	fmt.Println(split(17))
}
1
2
7 10
Program exited.

变量

1
2
3
4
5
6
7
8
//var 语句

var c, py, java bool

func main() {
    var i int
    fmt.Println(i, c, py, java)
}
1
0 false false false
1
2
3
4
5
6
:= ==> var
var k int = 3
k:=3

//想要在后面修改k的值,要
k = 4

类型转换

常量

1
 const Pi = 3.14

流程控制语句

for

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func main() {
    sum := 0
    for i := 0; i < 10; i++ {
        sum += i
    }
    fmt.Println(sum)
}
//初始化语句和后置语句是可选的!
	// for ; sum < 1000; {
	// 	sum += sum
	// }

甚至可以…

1
2
3
4
	for sum < 1000 {
		sum += sum
	}
    //相当于while一样用了

无限循环~

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

if

1
2
3
if x < 0 {

}

练习

牛顿法是通过选择一个起点 z 然后重复以下过程来求 Sqrt(x) 的近似值:

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

import (
	"fmt"
)

func Sqrt(x float64) float64 {
	z := 1.0 
	for i := 1; i <= 10; i++ {
		z -= (z*z - x) / (2 * z)
	}
	return z
}

func main() {
	fmt.Println(Sqrt(8964))
}
1
2
3
94.67840310467993

Program exited.

switch case

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func main() {
	fmt.Print("Go runs on ")
	switch os := runtime.GOOS; os {
	case "darwin":
		fmt.Println("OS X.")
	case "linux":
		fmt.Println("Linux.")
	default:
		// freebsd, openbsd,
		// plan9, windows...
		fmt.Printf("%s.", os)
	}
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func main() {
	fmt.Println("When's Saturday?")
	today := time.Now().Weekday()
	switch time.Saturday {
	case today + 0:
		fmt.Println("Today.")
	case today + 1:
		fmt.Println("Tomorrow.")
	case today + 2:
		fmt.Println("In two days.")
	default:
		fmt.Println("Too far away.")
	}
}

没有条件的switch

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func main() {
	t := time.Now()
	switch {
	case t.Hour() < 12:
		fmt.Println("Good morning!")
	case t.Hour() < 17:
		fmt.Println("Good afternoon.")
	default:
		fmt.Println("Good evening.")
	}
}

defer

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

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

1
2
3
4
5
func main() {
	defer fmt.Println("world")

	fmt.Println("hello")
}
1
2
3
4
hello
world

Program exited.

defer栈

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

1
2
3
4
5
6
7
8
9
func main() {
	fmt.Println("counting")

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

	fmt.Println("done")
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
counting
done
9
8
7
6
5
4
3
2
1
0

defer可以用在文件的关闭上面

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func CopyFile(dstName, srcName string) (written int64, err error) {
    src, err := os.Open(srcName)
    if err != nil {
        return
    }
    defer src.Close()

    dst, err := os.Create(dstName)
    if err != nil {
        return
    }
    defer dst.Close()

    return io.Copy(dst, src)
}

延迟语句使我们能够在打开文件后立即考虑关闭它,确保无论函数中有多少个返回语句,文件都将被关闭。

更多类型

指针

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func main() {
	i, j := 42, 2701

	p := &i         // point to i
	fmt.Println(*p) // read i through the pointer
	*p = 21         // set i through the pointer
	fmt.Println(i)  // see the new value of i

	p = &j         // point to j
	*p = *p / 37   // divide j through the pointer
	fmt.Println(j) // see the new value of j
}
1
2
3
4
5
42
21
73

Program exited.

结构体

1
2
3
4
5
6
7
8
type Vertex struct {
	X int
	Y int
}

func main() {
	fmt.Println(Vertex{1, 2})
}
1
2
3
{1 2}

Program exited.

使用点访问结构体

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
type Vertex struct {
	X int
	Y int
}

func main() {
	v := Vertex{1, 2}
	v.X = 4
	fmt.Println(v.X)
}
1
4

结构体指针

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

1
2
3
4
5
6
func main() {
	v := Vertex{1, 2}
	p := &v
	p.X = 1e9
	fmt.Println(v)
}
1
2
3
{1000000000 2}

Program exited.

结构体用法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
type Vertex struct {
	X, Y int
}

var (
	v1 = Vertex{1, 2}  // has type Vertex
	v2 = Vertex{X: 1}  // Y:0 is implicit
	v3 = Vertex{}      // X:0 and Y:0
	p  = &Vertex{1, 2} // has type *Vertex
)

func main() {
	fmt.Println(v1, p, v2, v3)
}
1
2
3
{1 2} &{1 2} {1 0} {0 0}

Program exited.

数组

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

表达式 var a [10]int 会将变量 a 声明为拥有有 10 个整数的数组。

数组的长度是其类型的一部分,因此数组不能改变大小。 这看起来是个限制,不过没关系, Go 提供了更加便利的方式来使用数组。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func main() {
	var a [2]string
	a[0] = "Hello"
	a[1] = "World"
	fmt.Println(a[0], a[1])
	fmt.Println(a)

	primes := [6]int{2, 3, 5, 7, 11, 13}
	fmt.Println(primes)
}
1
2
3
4
5
Hello World
[Hello World]
[2 3 5 7 11 13]

Program exited.

切片

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

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

切片通过两个下标来界定,一个下界和一个上界,二者以冒号分隔:

a[low : high] 它会选出一个半闭半开区间,包括第一个元素,但排除最后一个元素。

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

a[1:4]

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package main

import "fmt"

func main() {
	primes := [6]int{2, 3, 5, 7, 11, 13}

	var s []int = primes[1:4]
	fmt.Println(s)
}
1
2
3
[3 5 7]

Program exited.

切片类似数组的引用

切片就像数组的引用 切片并不存储任何数据,它只是描述了底层数组中的一段。

更改切片的元素会修改其底层数组中对应的元素。

和它共享底层数组的切片都会观测到这些修改。

 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)

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

	b[0] = "XXX"
	fmt.Println(a, b)
	fmt.Println(names)
}
1
2
3
4
[John Paul George Ringo]
[John Paul] [Paul George]
[John XXX] [XXX George]
[John XXX George Ringo]

切片字面量

切片字面量类似于没有长度的数组字面量。 这是一个数组字面量:

1
[3]bool{true, true, false}

下面这样则会创建一个和上面相同的数组,然后再构建一个引用了它的切片:

1
[]bool{true, true, false}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
func main() {
	q := []int{2, 3, 5, 7, 11, 13}
	fmt.Println(q)

	r := []bool{true, false, true, true, false, true}
	fmt.Println(r)

	s := []struct {
		i int
		b bool
	}{
		{2, true},
		{3, false},
		{5, true},
		{7, true},
		{11, false},
		{13, true},
	}
	fmt.Println(s)
}
1
2
3
[2 3 5 7 11 13]
[true false true true false true]
[{2 true} {3 false} {5 true} {7 true} {11 false} {13 true}]

切片默认行为

对于数组

var a [10]int 来说,以下切片表达式和它是等价的:

1
2
3
4
a[0:10]
a[:10]
a[0:]
a[:]

切片的长度和容量

切片 s 的长度和容量可通过表达式 len(s) 和 cap(s) 来获取。

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

func printSlice(s []int) {
	fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
1
2
3
4
len=6 cap=6 [2 3 5 7 11 13]
len=0 cap=6 []
len=4 cap=6 [2 3 5 7]
len=2 cap=4 [5 7]

nil切片

切片的零值是 nil。

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

用make创建切片

可以使用内置函数 make 创建切片,该函数具有以下签名:

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

其中 T 表示要创建的切片元素类型。 make 函数接受一个类型、一个长度和一个可选的容量。调用时, make 会分配一个数组并返回一个引用该数组的切片。

1
2
3
var s []byte
s = make([]byte, 5, 5)
// s == []byte{0, 0, 0, 0, 0}

当省略容量参数时,它默认为指定的长度。以下是相同代码的更简洁版本:

1
s := make([]byte, 5)

切片的切片

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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], " "))
	}
}
1
2
3
X _ X
O _ X
_ _ O

向切片追加元素

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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)
}
1
2
3
4
len=0 cap=0 []
len=1 cap=1 [0]
len=2 cap=2 [0 1]
len=5 cap=6 [0 1 2 3 4]

range遍历

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

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

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)
	}
}
1
2
3
4
5
6
7
8
2**0 = 1
2**1 = 2
2**2 = 4
2**3 = 8
2**4 = 16
2**5 = 32
2**6 = 64
2**7 = 128

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

1
2
for i, _ := range pow
for _, value := range pow

若你只需要索引,忽略第二个变量即可。

1
for i := range pow

练习:切片

实现 Pic。它应当返回一个长度为 dy 的切片,其中每个元素是一个长度为 dx,元素类型为 uint8 的切片。当你运行此程序时,它会将每个整数解释为灰度值 (好吧,其实是蓝度值)并显示它所对应的图像。

图像的解析式由你来定。几个有趣的函数包括 (x+y)/2、xy、x^y、xlog(y) 和 x%(y+1)。

(提示:需要使用循环来分配 [][]uint8 中的每个 []uint8。)

(请使用 uint8(intValue) 在类型之间转换;你可能会用到 math 包中的函数。)

 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 (
	"golang.org/x/tour/pic"
	"math"
)

func Pic(dx, dy int) [][]uint8 {
	// 创建一个长度为 dy 的切片,其中每个元素是一个长度为 dx 的切片
	picture := make([][]uint8, dy)
	for y := range picture {
		picture[y] = make([]uint8, dx)
		for x := range picture[y] {
			// 使用 (x+y)/2 作为图像的解析式
			picture[y][x] = uint8((x + y) / 2)
		}
	}
	return picture
}

func main() {
	pic.Show(Pic)
}

map映射

map 映射将键映射到值。

映射的零值为 nil 。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

func main() {
	m = make(map[string]Vertex)
	m["Bell Labs"] = Vertex{
		40.68433, -74.39967,
	}
	fmt.Println(m["Bell Labs"])
}
1
{40.68433 -74.39967}

映射字面量

 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)
}
1
map[Bell Labs:{40.68433 -74.39967} Google:{37.42202 -122.08408}]

修改映射

在映射 m 中插入或修改元素:

1
m[key] = elem

获取元素:

1
elem = m[key]

删除元素:

1
delete(m, key)

通过双赋值检测某个键是否存在:

1
elem, ok = m[key]

若 key 在 m 中,ok 为 true ;否则,ok 为 false。

若 key 不在映射中,则 elem 是该映射元素类型的零值。

注:若 elem 或 ok 还未声明,你可以使用短变量声明:

1
elem, ok := m[key]

练习:映射

实现 WordCount。它应当返回一个映射,其中包含字符串 s 中每个“单词”的个数。 函数 wc.Test 会为此函数执行一系列测试用例,并输出成功还是失败。

你会发现 strings.Fields 很有用。

 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 (
	"strings"
	"golang.org/x/tour/wc"
)

func WordCount(s string) map[string]int {
	// 使用 strings.Fields 将字符串分割成单词
	words := strings.Fields(s)
	// 创建一个映射来存储每个单词的计数
	wordCount := make(map[string]int)
	// 遍历单词列表,统计每个单词的出现次数
	for _, word := range words {
		wordCount[word]++
	}
	return wordCount
}

func main() {
	wc.Test(WordCount) 
}

函数闭包

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

import (
    "fmt"
)

func a() func() int {
    i := 0
    b := func() int {
        i++
        fmt.Println(i)
        return i
    }
    return b
}

func main() {
    c := a()
    c()
    c()
    c()

    a() //不会输出i
}
1
2
3
1
2
3

不会输出i的原因是因为a()上再要了一个函数需求,单a()不会出

练习:斐波那契闭包

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

import "fmt"

// fibonacci 是返回一个「返回一个 int 的函数」的函数
func fibonacci() func() int {
	a, b := 0, 1
	return func() int {
		a, b = a+b, a
		return a
	}
}

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

方法和接口

方法

Go 没有类。不过你可以为类型定义方法。

方法就是一类带特殊的 接收者 参数的函数。

方法接收者在它自己的参数列表内,位于 func 关键字和方法名之间。

在此例中,Abs 方法拥有一个名字为 v,类型为 Vertex 的接收者。

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

方法即函数

记住:方法只是个带接收者参数的函数。

现在这个 Abs 的写法就是个正常的函数,功能并没有什么变化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
type Vertex struct {
	X, Y float64
}

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

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

指针与函数

 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
type Vertex struct {
	X, Y float64
}

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

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

//等价于
// func Scale(v Vertex, f float64) Vertex {
// 	v.X = v.X * f
// 	v.Y = v.Y * f
// 	return v
// }
// func main() {
// 	v := Vertex{3, 4}
// 	Scale(v, 10)  //注意这里没有&
// 	fmt.Println(Abs(v))
// }

func main() {
	v := Vertex{3, 4}
	Scale(&v, 10) //在调用 Scale 函数时,我们使用 &v 来获取变量 v 的地址,并将其传递给 Scale 函数。这样,Scale 函数就可以通过这个指针来修改 v 的值。
	fmt.Println(Abs(v))
}

方法与指针重定向

带指针的函数必须接受一个指针

1
2
3
var v Vertex
ScaleFunc(v, 5)  // 编译错误!
ScaleFunc(&v, 5) // OK

而接收者为指针的的方法被调用时,接收者既能是值又能是指针:

1
2
3
4
var v Vertex
v.Scale(5)  // OK
p := &v
p.Scale(10) // OK

对于语句 v.Scale(5) 来说,即便 v 是一个值而非指针,带指针接收者的方法也能被直接调用。 也就是说,由于 Scale 方法有一个指针接收者,为方便起见,Go 会将语句 v.Scale(5) 解释为 (&v).Scale(5)。

选择值或指针作为接收者

使用指针接收者的原因有二:

首先,方法能够修改其接收者指向的值。

其次,这样可以避免在每次调用方法时复制该值。若值的类型为大型结构体时,这样会更加高效。

在本例中,Scale 和 Abs 接收者的类型为 *Vertex,即便 Abs 并不需要修改其接收者。

通常来说,所有给定类型的方法都应该有值或指针接收者,但并不应该二者混用。 (我们会在接下来几页中明白为什么。)

 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
type Vertex struct {
	X, Y float64
}

// func (v *Vertex) Scale(f float64) float64 {
// 	v.X = v.X * f
// 	v.Y = v.Y * f
// 	return v.Abs()
// } //这个不同在直接输出了值
//调用的话要 result = v.Abs(10)
//下面的直接 v.Abs(10)

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

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

func main() {
	v := &Vertex{3, 4}
	fmt.Printf("缩放前:%+v,绝对值:%v\n", v, v.Abs())
	v.Scale(5)
	fmt.Printf("缩放后:%+v,绝对值:%v\n", v, v.Abs())
}

接口

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

接口值

接口也是值。它们可以像其它值一样传递。

接口值可以用作函数的参数或返回值。

在内部,接口值可以看做包含值和具体类型的元组:

(value, type) 接口值保存了一个具体底层类型的具体值。

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

底层值为 nil 的接口值

即便接口内的具体值为 nil,方法仍然会被 nil 接收者调用。

在一些语言中,这会触发一个空指针异常,但在 Go 中通常会写一些方法来优雅地处理它(如本例中的 M 方法)。

注意: 保存了 nil 具体值的接口其自身并不为 nil。

空接口

指定了零个方法的接口值被称为 空接口:

interface{} 空接口可保存任何类型的值。(因为每个类型都至少实现了零个方法。)

空接口被用来处理未知类型的值。例如,fmt.Print 可接受类型为 interface{} 的任意数量的参数。

类型断言

在 Go 语言中,类型断言(Type Assertion)是一种用于将接口类型的变量转换为具体类型的机制。类型断言的基本语法是 x.(T),其中 x 是一个接口类型的变量,T 是目标类型。类型断言有两种形式:

单值形式:value := x.(T)

双值形式:value, ok := x.(T)

1
2
3
4
5
var x interface{} = "Hello, World"
str := x.(string) // 成功,str 的值为 "Hello, World"
fmt.Println(str)

num := x.(int) // 失败,引发 panic: interface conversion: interface {} is string, not int
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var x interface{} = "Hello, World"
str, ok := x.(string) // 成功,str 的值为 "Hello, World",ok 为 true
if ok {
    fmt.Println(str)
}

num, ok := x.(int) // 失败,num 的值为 int 类型的零值(0),ok 为 false
if !ok {
    fmt.Println("x is not an int")
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package main

import "fmt"

func main() {
    var data interface{} = "Hello, World"

    // 使用双值形式的类型断言
    if str, ok := data.(string); ok {
        fmt.Println("Data is a string:", str)
    } else if num, ok := data.(int); ok {
        fmt.Println("Data is an int:", num)
    } else {
        fmt.Println("Data is of an unknown type")
    }
}

练习:Stringer

通过让 IPAddr 类型实现 fmt.Stringer 来打印点号分隔的地址。

例如,IPAddr{1, 2, 3, 4} 应当打印为 “1.2.3.4”。

 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: 为 IPAddr 添加一个 "String() string" 方法。

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 值,调用它的代码应当判断这个错误是否等于 nil 来进行错误处理。

1
2
3
4
5
6
i, err := strconv.Atoi("42")
if err != nil {
    fmt.Printf("couldn't convert number: %v\n", err)
    return
}
fmt.Println("Converted integer:", i)

error 为 nil 时表示成功;非 nil 的 error 表示失败。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
type MyError struct {
	When time.Time
	What string
}

func (e *MyError) Error() string {
	return fmt.Sprintf("at %v, %s",
		e.When, e.What)
}

func run() error {
	return &MyError{
		time.Now(),
		"it didn't work",
	}
}

func main() {
	if err := run(); err != nil {
		fmt.Println(err)
	}
}

练习:错误

从之前的练习中复制 Sqrt 函数,修改它使其返回 error 值。

Sqrt 接受到一个负数时,应当返回一个非 nil 的错误值。复数同样也不被支持。

创建一个新的类型

1
type ErrNegativeSqrt float64

并为其实现

1
func (e ErrNegativeSqrt) Error() string

方法使其拥有 error 值,通过 ErrNegativeSqrt(-2).Error() 调用该方法应返回 “cannot Sqrt negative number: -2”。

注意: 在 Error 方法内调用 fmt.Sprint(e) 会让程序陷入死循环。可以通过先转换 e 来避免这个问题:fmt.Sprint(float64(e))。这是为什么呢?

修改 Sqrt 函数,使其接受一个负数时,返回 ErrNegativeSqrt 值。

 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"
	"math"
)


type ErrNegativeSqrt float64

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 0 ,ErrNegativeSqrt(x)
	}
	return math.Sqrt(x),nil
}

func main() {
	fmt.Println(Sqrt(2))
	fmt.Println(Sqrt(-2))
}
1
2
1.4142135623730951 <nil>
0 cannot Sqrt negative number: -2

Readers

1
2
3
type Reader interface {
    Read(p []byte) (n int, err error)
}

Read 用数据填充给定的字节切片并返回填充的字节数和错误值。在遇到数据流的结尾时,它会返回一个 io.EOF 错误。

示例代码创建了一个 strings.Reader 并以每次 8 字节的速度读取它的输出。

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

import (
    "fmt"
    "io"
    "os"
)

func main() {
    // 打开一个文件
    file, err := os.Open("example.txt")
    if err != nil {
        fmt.Println("Error opening file:", err)
        return
    }
    defer file.Close()

    // 创建一个缓冲区
    buffer := make([]byte, 1024)

    // 使用 Reader 接口读取数据
    for {
        n, err := file.Read(buffer)
        if err != nil {
            if err == io.EOF {
                break
            }
            fmt.Println("Error reading file:", err)
            return
        }
        fmt.Print(string(buffer[:n]))
    }
}

练习:Reader

实现一个 Reader 类型,它产生一个 ASCII 字符 ‘A’ 的无限流。

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

import (
	"golang.org/x/tour/reader"
)

type MyReader struct{}

// 为 MyReader 添加一个 Read([]byte) (int, error) 方法
func (r MyReader) Read(b []byte) (int, error) {
	for i := range b {
		b[i] = 'A'
	}
	return len(b), nil
}

func main() {
	reader.Validate(MyReader{})
}

练习:rot13Reader

有种常见的模式是一个 io.Reader 包装另一个 io.Reader,然后通过某种方式修改其数据流。

例如,gzip.NewReader 函数接受一个 io.Reader(已压缩的数据流)并返回一个同样实现了 io.Reader 的 *gzip.Reader(解压后的数据流)。

编写一个实现了 io.Reader 并从另一个 io.Reader 中读取数据的 rot13Reader,通过应用 rot13 代换密码对数据流进行修改。

rot13Reader 类型已经提供。实现 Read 方法以满足 io.Reader。

图像