这里我使用的是大佬提供的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
3
4
5
6
7
8
| //var 语句
var c, py, java bool
func main() {
var i int
fmt.Println(i, c, py, java)
}
|
1
2
3
4
5
6
| := ==> var
var k int = 3
k:=3
//想要在后面修改k的值,要
k = 4
|
类型转换
常量
流程控制语句#
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 {
}
}
|
牛顿法是通过选择一个起点 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)
}
|
结构体指针#
如果我们有一个指向结构体的指针 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
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
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
|
若你只需要索引,忽略第二个变量即可。
练习:切片#
实现 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
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 中插入或修改元素:
获取元素:
删除元素:
通过双赋值检测某个键是否存在:
若 key 在 m 中,ok 为 true ;否则,ok 为 false。
若 key 不在映射中,则 elem 是该映射元素类型的零值。
注:若 elem 或 ok 还未声明,你可以使用短变量声明:
练习:映射#
实现 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
}
|
不会输出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())
}
|
方法即函数#
记住:方法只是个带接收者参数的函数。
现在这个 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。