目前字节跳动已全面拥抱 Go 语言。除此之外,哔哩哔哩、七牛云、腾讯、百度、美团、Facebook、Google、Twitter、滴滴、深信服、知乎、去哪儿、360、微博等公司也在大量使用 Go 语言。

👨‍💻本文中的源码地址:https://github.com/wangkechun/go-by-example

学习目录

1. Go 简介

介绍下 Go 语言的特性以及目前 Go 语言在市场上的 “帝位”。

1.1 Go 语言特性

  1. 高性能 & 高并发:Go 语言天生支持高并发(goroutine & channel & 调度器),不像很多语言通过库的形式支持;Go 像一些低级别的语言 (C/C++) 一样是一门编译型语言,这意味着它的性能足以媲美 C/C++!
  2. 语法简洁易懂:语法类似 C 语言,比如:同时去掉了不需要的 (),循环也只简化成 for 循环这一种表示… 该特点使得用 Go 编写的代码易于维护。
  3. 丰富的标准库:Go 语言带有极其丰富与完善的标准库,无需再借助第三方库完成便可以应对日常的开发,而且能持续享受到语言迭代带来的性能优化。
  4. 完整的工具链:编译、代码格式化、错误检查、包管理、代码补全等都有相应的工具,Go 语言还内置了单元测试框架(单元测试、性能测试、代码覆盖率、性能优化)。
  5. 静态链接:所有的编译结果默认都是静态链接的,只需要拷贝编译后唯一的可执行文件即可部署运行,线上发布的体积可以控制得很小。
  6. 快速编译:Go 语言一开始设计就考虑到快速编译。它能像其他解释型语言一样(Python & JavaScript),你不会注意它正在编译。
  7. 跨平台:Go 语言能在 Linux、MacOS、Windows 等操作系统下运行,还能用于开发 Android、iOS 软件,甚至能运行在路由器、树莓派等等设备;同时具备交叉编译 特性。
  8. 垃圾回收:Go 无需考虑内存的分配与释放,因为其内存由 Go 自身进行管理,不同于 C/C++,和 Java 类似。

简洁的 Go 语法

1.2 拥抱 Go 语言

目前字节跳动已全面拥抱 Go 语言。除此之外,哔哩哔哩、七牛云、腾讯、百度、美团、Facebook、Google、Twitter、滴滴、深信服、知乎、去哪儿、360、微博等公司也在大量使用 Go 语言。

Go 语言在云计算、微服务、大数据、区块链、物联网等领域蓬勃发展,Docker、Kubernetes、etcd 等几乎所有的云原生组件全都是用 Go 语言实现。

2. Go 入门

😆这部分简单概括下如何搭建 Go 的开发环境,浏览下 Go 语言的基础语法 & 标准库

2.1 搭建开发环境

2.1.1 下载安装 Golang

2.1.2 配置 Golang IDE

⭐首选目前市场上使用最广泛的两款 IDE : VSCode & GoLand

  • VSCode:由微软公司开发,免费。能运行在 MacOS、Windows、Linux 上的跨平台开源代码编辑器(功能齐全的 IDE),需要在扩展市场中安装 Go 插件才能支持 Golang 开发。
  • GoLand:由 JetBrains 公司开发,付费 (学生可申请免费使用)。

2.1.3 云上开发环境

我们还可以使用基于 GitHub 的 Gitpod 在线编程环境来使用 golang,只需在你的开源仓库 URL 的 https:// 替换成 https://gitpod.io/# 即可。

2.2 基础语法 & 常用标准库

这部分开始正式学习 Golang 的语法以及常用标准库的使用。

2.2.0 从 Hello World 见证 Go 程序的运行

1
2
3
4
5
6
7
8
9
package main

import (
"fmt"
)

func main() {
fmt.Println("hello world")
}

有 2 种方式运行该段程序:

  • go run 命令可直接运行程序
  • go build 可将程序编译成二进制,编译完成后直接执行 ./main 即可运行

2.2.1 变量 & 常量

Golang 是一门强类型语言(每个变量都有其对应的类型),变量 var 如果没有标注类型会自动推导,常量 const 默认没有确定类型(自动推导)。

注意:如果定义了变量,必须得使用,否则编译不通过

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package main

import (
"fmt"
"math"
)

// 全局变量
var x string = "global" // 可以

// x := "global" // 不可以, := 仅可以在函数内部使用

const (
A = iota // iota=0, 值为 0
B // iota=1, 值为 1
C = iota // iota=2, 值为 2
D = "test" // iota=3, 值为"test"
E // iota=4, 值为"test"
F = 9 // iota=5, 值为 9
G // iota=6, 值为 9
)

func main() {
/* ------------ 变量 ------------ */
// 声明并赋值
var a = "掘了"

// 类型推导
var b = true

// 多变量定义 & 类型推导
var c, d int = 1, 9
var e, f = 6, "F"
var (
g = 666
h = false
)

// 简短定义(另一声明变量的方式 :=)
i := 1.9

// 类型转换: float64=>float32
j := float32(i)

// 字符串可通过 + 拼接
k := "掘金" + a

// 匿名变量 _
y, _ := 1, 2

// Go 易于实现两数
c, d = d, c

/* ------------ 常量 ------------ */
const l string = "constant"
const m = 10
const n = 2e3 / m

// "global" "掘了" true 9 1 6 "F" 666 false 1.9 1.9 "掘金掘了" "constant" 10 200 1
fmt.Println(x, a, b, c, d, e, f, g, h, i, j, k, l, m, math.Abs(n), y)

// 0 1 2 "test" "test" 9 9
fmt.Println(A, B, C, D, E, F, G)
}

2.2.2 数据类型 & 占位符

🌗Go 语言基本数据类型较多,主要有以下这些:

  • byteintint8int16int32int64uint
  • float32float64
  • error
  • string
  • bool
  • rune

🌓Go 语言囊括的复合数据类型范围较广 (下文慢慢介绍) :

  • 数组 array
  • 切片 slice
  • 字典 map
  • 函数 func
  • 结构体 struct
  • 通道 channel
  • 接口 interface
  • 指针 *

🚀根据数据特点又可分为值传递 & 引用传递

  • 值传递:intfloatstringboolarraystruct
  • 引用传递:slicemapchan*

⭐至于运算符(算法运算符、关系运算符、逻辑运算符、位运算符、赋值运算符)和其他语言基本一致,这里不再赘叙。

2.2.3 if-else 条件判断

Go 语言中的 if 判断语句不需要 ();同理,switchfor 也不需要。

Go 语言中的 if 语句存在一种特殊的写法:

errmyFunc() 的返回值,执行后再对 err==nil 语句进行判断。

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

import "fmt"

func main() {
// Golang 的 if、for、switch 均无需 ()
if 5%2 == 0 {
fmt.Println("5 is even")
} else {
fmt.Println("5 is odd")
}

// 特殊的 if 分支: if 执行语句; 判断语句 { }
if num := 9; num < 0 {
fmt.Println(num, "is negative")
} else if num < 10 {
fmt.Println(num, "has 1 digit")
} else {
fmt.Println(num, "has multiple digits")
}
}

2.2.4 for 循环

Go 语言中没有 whiledo while 这类循环,仅仅只有 for 这一种循环。

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

import "fmt"

func main() {
// while
i := 1
for i <= 3 {
fmt.Println(i)
i = i + 1
}

for j := 7; j < 9; j++ {
fmt.Println(j)
}

// 死循环
for {
fmt.Println("endless loop")
}
}

2.2.5 switch 分支

相比 C/C++,Go 语言的 switch 分支略有不同,也更加强大

  • 不同之处:case 中不需要再显式加 break,执行完对应 case 就会退出,不像 C/C++ 没加 break 会跑完余下所有 case;同时 switch 后也不再需要 ()
  • 强大之处:Go 语言中 switch 后能跟任意变量类型;也可不跟任何变量,然后在 case 中写条件分支,这样就将 switch-case 分支结构简化为 if-else 条件判断结构了。
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"
"time"
)

func main() {

// switch-case 标准结构
a := 2
switch a {
case 1:
fmt.Println("one")
case 2:
fmt.Println("two") // 控制台只输出 "two"
case 3:
fmt.Println("three")
case 4, 5:
fmt.Println("four or five")
default:
fmt.Println("other")
}

// switch 后不跟变量
t := time.Now()
switch {
case t.Hour() < 12:
fmt.Println("It's before noon")
default:
fmt.Println("It's after noon")
}
}

2.2.6 数组 array

数组是一个长度固定的元素序列,可利用索引取值/存值。

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

import "fmt"

func main() {
// 一维数组
var a [5]int
a[4] = 100
fmt.Println("get:", a[2]) // get: 0
fmt.Println("len:", len(a)) // len: 5

// 3 种数组初始化方式 (1)
b := [3]int{1, 2, 3} // 或 var b = [3]int{1, 2, 3}
fmt.Println(b) // [1 2 3]

// 3 种数组初始化方式 (2)
c := [...]int{1, 3, 2} // 或 var c = [...]int{1, 2, 3}
fmt.Println(c) // [1 3 2]

// 3 种数组初始化方式 (3)
d := [...]int{1: 3, 6: 5} // 或 var d = [...]int{1: 3, 6: 5}
fmt.Println(d) // [0 3 0 0 0 0 5]

// 二维数组
var twoD [2][3]int
for i := 0; i < 2; i++ {
for j := 0; j < 3; j++ {
twoD[i][j] = i + j
}
}
fmt.Println("2d: ", twoD) // [[0 1 2] [1 2 3]]
}

🚫不过在真实的业务场景中,我们很少直接使用定长的数组,更多使用的是切片。

2.2.7 切片 slice

数组是定长的,所以 Go 推出可扩容的切片。与数组不同的是,切片不需要指定 [] 里的长度。

1
2
3
4
5
6
7
8
9
10
// 1.声明空切片
var slice1 []string
// 2.创建一个带默认长度的切片
var slice2 = make([]int, 3)
// 3.声明并初始化切片
slice3 := []string{"g", "o", "o", "d"}
// 4.最常用切片创建方式
slice4 := make([]int, 3)
// 5.创建带有长度和容量的切片
slice5 := make([]int, 3, 5)

通常情况下,我们会使用 make 函数来创建一个切片;使用 append 来追加元素,注意要将其结果赋值给原切片;然后可以像数组一样去取值;还可以通过 [a:b] 来获取切片中指定范围的值。

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

import "fmt"

func main() {
// 创建切片 —— 长度:3; 容量:5
s := make([]string, 3, 5)
s[0] = "a"
s[1] = "b"
s[2] = "c"
fmt.Println("get:", s[2]) // c
fmt.Println("len:", len(s)) // 3
fmt.Println("cap:", cap(s)) // 5

// append 追加元素
s = append(s, "d")
s = append(s, "e", "f")
fmt.Println(s) // [a b c d e f]

// copy 拷贝元素
c := make([]string, len(s))
copy(c, s)
fmt.Println(c) // [a b c d e f]

cs := []string{"w", "w", "w", "w", "w"}
copy(cs, s[:3])
fmt.Println(cs) // [a b c w w]

// slice 切片取值操作也可以像 python 中的一样取出指定范围的元素, 只不过不可以是负数索引
fmt.Println(s[2:5]) // [c d e]
fmt.Println(s[:5]) // [a b c d e]
fmt.Println(s[2:]) // [c d e f]

// 声明切片并初始化
good := []string{"g", "o", "o", "d"}
fmt.Println(good) // [g o o d]

// 将切片 c 完全添加到切片 good 中!
good = append(good, c...)
fmt.Println(good) // [g o o d a b c d e f]
}

🤔切片元素的删除:Go 语言中并没有提供一个内置函数将切片中的元素进行删除,但我们可以使用 [x,y] 或者 append 来实现。

1
2
3
4
5
6
7
// 切片元素的删除
slice := []int{1, 2, 3, 4, 5} // 原始切片: [1 2 3 4 5]
slice = slice[1:] // 删除索引为0的元素: [2 3 4 5]
idx := 1 // 索引值 idx=1
slice = append(slice[:idx], slice[idx+1:]...) // 删除索引为1的元素: [2 4 5]

fmt.Println(slice) // [2 4 5]

⭐小结一下:

  • 每一个切片都引用了一个底层数组。
  • 切片创建时存储了一个长度和一个容量,还有一个指向数组的指针,当切片添加数据时,如果没有超过容量,直接添加,超出容量自动扩容成倍增长。
  • 一旦切片扩容,指针会指向一个新的底层数组内存地址。

2.2.8 字典 map

map 是 Go 语言中内置的字典类型,存储的是键值对 key:value 类型的数据,有以下特点:

  • map 是完全无序的,遍历时不会按照字母顺序或插入顺序输出,而是随机的,且只能通过 key 访问对应的 value。
  • 空的 slice 是可以直接使用的,因为它有底层数组;但空的 map 不能直接使用,需要先 make 或初始化后才能使用(map 是引用类型,如果声明没有初始化值,默认为 nil,是不能直接使用的)。
  • map 的 key 不能重复,否则新增加的值会覆盖原来 key 对应的 value。

创建 map

1
2
3
4
5
6
// 声明空 map: 不可直接使用
var map1 map[int]string
// 创建 map (已初始化——零值填充): 可直接使用
map2 := make(map[int]string)
// 声明并初始化 map: 可直接使用
map3 := map[string]int{"one": 1, "two": 2}

使用 map

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

import "fmt"

func main() {
// make 创建 map
m := make(map[string]int)
m["one"] = 1
m["two"] = 2
fmt.Println(m) // map[one:1 two:2]
fmt.Println(len(m)) // 2
fmt.Println(m["one"]) // 1
fmt.Println(m["unknow"]) // 0

// 如果 map 为空, 不能直接使用, 否则报错 panic: assignment to entry in nil map
var nilMap map[int]float32
//nilMap[0] = 1.0 // panic: assignment to entry in nil map
if nilMap == nil {
nilMap = make(map[int]float32)
}
nilMap[0] = 1.0
fmt.Println(nilMap) // map[0:1]

// 判断 key 是否存在, value,ok := map[key]
r, ok := m["unknow"]
fmt.Println(r, ok) // 0 false
t, ok := m["two"]
fmt.Println(t, ok) // 2 true

// map 中使用 delete 删除 key 对应的键值对
delete(m, "one")
delete(m, "two")
fmt.Println(m) // map[]

// 不使用 make 函数, 直接创建并初始化 map
m2 := map[string]int{"one": 1, "two": 2}
var m3 = map[string]int{"one": 1, "two": 2}
fmt.Println(m2, m3) // map[one:1 two:2] map[one:1 two:2]
}

2.2.9 range 遍历

介绍下 range 关键字:对于 slice 或者 map,我们可以使用 range 对其进行快速遍历。

  • slice:第一个是索引,第二个是值
  • map:第一个是键,第二个是值

如果不需要索引/键,可以直接使用 _ 匿名变量代替。

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

import (
"fmt"
"strconv"
)

func main() {
// slice —— range
nums := []int{2, 3, 4}
sum := 0
for i, num := range nums {
sum += num
if num == 2 {
fmt.Println("index:", i, "num:", num) // index: 0 num: 2
}
}
fmt.Println(sum) // 9
// 如若不需要索引, 可使用 _ 匿名变量
for _, num := range nums {
fmt.Println(num)
}

// map —— range
maps := make(map[int]string)
maps[0] = "hello"
maps[1] = "world"
for key, value := range maps {
// int 转 string(1): fmt.Sprintf("%d", intVal)
fmt.Println(fmt.Sprintf("%d", key) + ":" + value)
// int 转 string(2): strconv.Itoa(intVal)
fmt.Println(strconv.Itoa(key) + ":" + value)
// int 转 string(3): , 分隔
fmt.Println(key, ":", value)
}
// 如若不需要键, 可使用 _ 匿名变量
for _, v := range maps {
fmt.Println("v", v)
}
// 如若不需要值, 可使用 _ 匿名变量, 也可以直接省略
for k := range maps {
fmt.Println("key", k)
}
}

2.2.10 函数 func

💖Go 语言中的函数比较特殊,参数类型、返回值类型都是后置的,而且函数首字母大写/小写的作用不同:

  • 如果函数名首字母大写则表示公共函数,其他包能够调用,前提得引入当前包。
  • 如果函数名首字母小写则表示私有函数,仅能够在本包中调用。

Go 语言的函数 func 结构

💖Golang 中的函数原生支持返回多个值,且在实际业务场景中都返回两个值,第一个是真正的返回结果,第二个是错误信息。

多返回值

接下来看看函数的基本定义及其用法:

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 add(a int, b int) (int, string) {
return a + b, "ok"
}

func add2(a, b int) int {
return a + b
}

func exists(m map[string]string, k string) (v string, ok bool) {
v, ok = m[k]
return v, ok
}

func main() {
res, _ := add(1, 2)
fmt.Println(res) // 3

v, ok := exists(map[string]string{"a": "A"}, "a")
fmt.Println(v, ok) // A True
}

💖Go 语言中的函数也是一种数据结构,也可以被存储在一个变量中,调用变量的时候也就相当于调用函数——也就是可以将函数作为传递参数

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

import "fmt"

func add(a int, b int) (int, string) {
return a + b, "ok"
}

func main() {
// 函数作为传递参数
f := add
ret, _ := f(2, 3)
fmt.Println(ret) // 5

// 定义函数变量
var addingFunc func(int, int) (int, string)
addingFunc = add
result, str := addingFunc(1, 5)
fmt.Println(result, str) // 6 ok
}

💖Go 中定义匿名函数加上 () 相当于直接调用;如果没有 () 则表示定义一个函数,可将其赋值给变量然后进行多次调用——匿名函数

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

import (
"fmt"
"strconv"
)

func main() {
// 匿名函数的简单定义与调用
func() {
fmt.Println("匿名函数")
}() // 匿名函数

// 匿名函数的使用
sum := func(a int, b int) int {
return a + b
}(1, 2)
fmt.Println(sum) // 3

// 定义匿名函数并赋值给其他变量, 此处并没有调用匿名函数, 因为没有()
myFunc := func(a, b int) string {
return strconv.Itoa(a) + fmt.Sprintf("%d", b)
}
// 调用定义的匿名函数
returnVal := myFunc(6, 66)
fmt.Println(returnVal) // 666
}

💖甚至你可以将匿名函数作为另一个函数的参数/返回值;其中作为参数的函数叫做回调函数,调用的函数叫做高阶函数

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

import "fmt"

// 匿名函数作为返回值
func returnFunc(a int, b int) func() int {
return func() int {
return a + b
}
}

func increase(a int, b int) int {
return a + b
}

func reduce(a int, b int) int {
return a - b
}

// opera: 高阶函数
// f: 回调函数
func opera(a int, b int, f func(int, int) int) int {
res := f(a, b)
return res
}

func main() {
// 将匿名函数作为另一函数的参数
num1 := opera(1, 2, increase) // 3
num2 := opera(1, 2, reduce) // -1
fmt.Println(num1, num2)

// 定义匿名函数作为函数参数
num3 := opera(3, 4, func(a int, b int) int {
return a * b
})
fmt.Println(num3) // 12
}

💖既然 Go 语言中的函数能作为返回值和参数,自然能打造闭包结构,与 JS 闭包含义相同。

所谓闭包,就是一个外层函数中有内层函数,这个内层函数会操作外层函数的局部变量,并且,外层函数把内层函数作为返回值,将内层函数与外层函数中的局部变量统称为闭包结构。

💖先捋清下为什么我们需要闭包结构,闭包有什么作用?

首先看一个简单的计数器例子!

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

import "fmt"

var counter = 0

func add() int {
counter++
return counter
}

func main() {
add()
add()
add()
fmt.Println(counter) // 3
}

虽然我们已经达到了目的,但是任意一个函数中都可以随意改动 counter 的值,所以该计数器并不完美,那我们将 counter 放到函数中如何?

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

import "fmt"

func add() int {
counter := 0
counter++
return counter
}

func main() {
add()
add()
ret := add()
fmt.Println(ret) // 1
}

本意想输出 3,但由于局部变量在函数每次调用时都会被初始化为 0,所以达不到预期效果。所以我们此时就需要使用闭包来解决了。

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

import "fmt"

func add() func() int {
counter := 0
innerFunc := func() int {
counter++
return counter
}
return innerFunc
}

func main() {
inner := add()
inner()
inner()
ret := inner()
fmt.Println(ret) // 3
}

精简下闭包代码:

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

import "fmt"

func main() {
// 以上闭包的简写形式
add := func() func() int {
counter := 0
return func() int {
counter++
return counter
}
}()

add()
add()
ret := add()

fmt.Println(ret) // 3
}

💖现在估计你能很轻松的理解以下代码了。

注意:由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。因此可以手动解除对内层匿名函数的引用,以便释放内存。

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

import "fmt"

func main() {
res := closure()
// 执行 closure 函数返回的内层函数
r1 := res()
r2 := res()

fmt.Println(res) // 返回内层函数函数体地址: 0x46c7e0
fmt.Println(r1) // 1
fmt.Println(r2) // 2

// 手动解除对内层函数的引用, 以便释放内存
res = nil
}

// 定义一个闭包结构的函数, 返回一个匿名函数
func closure() func() int { //外层函数
// 定义外层函数的局部变量a
a := 0
// 定义内层函数并返回
return func() int {
// 内层函数用到了外层函数的局部变量, 此变量不会随着外层函数的结束而销毁
a++
return a
}
}

💖defer 函数是 Go 语言中另一奇特的存在:当 defer 函数调用后,代码暂不执行,推迟到主函数 main 执行结束后才会执行;一般用于资源的关闭。

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

import "fmt"

func main() {

defer func() {
fmt.Println("Close Resource")
}()
fmt.Println("defer...")

// 输出结果:
// defer...
// Close Resource
}

2.2.11 指针

Golang 也支持指针,用法同 C/C++,只不过支持的操作比较有限。

Go 语言中通过 & 获取变量的地址,通过 * 获取指针所对应的变量存储的数值。

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

import "fmt"

func add2(n int) {
n += 2
}

func add2ptr(n *int) {
*n += 2
}

func main() {
n := 5
add2(n)
fmt.Println(n) // 5
add2ptr(&n)
fmt.Println(n) // 7
}

🚀接着简单介绍下:「数组指针」、「指针数组」、「指针函数」、「指针参数

区分数组指针指针数组的定义,只需看清变量名更靠近 [](指针数组) 还是 *(数组指针)。

至于要区分这二者的使用方式只需要记得 [] 优先级高于 * 即可。

(1)数组指针:指向数组的指针

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

import "fmt"

func main() {
// 数组指针
arr := [3]int{1, 2, 3}
var ars *[3]int
ars = &arr
(*ars)[0] = 0
ars[1] = 1
fmt.Println(ars) // &[0 1 3]
fmt.Println(*ars) // [0 1 3]
fmt.Println((*ars)[1]) // 1
}

(2)指针数组:数组元素皆为指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 指针数组
a, b, c := 1, 2, 3
nums := [3]int{a, b, c}
numps := [3]*int{&a, &b, &c}

*numps[1] = 1
*numps[2] = 6
fmt.Println(nums) // [1 2 3]
fmt.Println(numps) // [0xc000018128 0xc000018130 0xc000018138]
fmt.Println(*numps[0]) // 1
fmt.Println(*numps[2]) // 6
for _, v := range numps {
fmt.Print(*v, " ") // 1 1 6
}

(3)指针函数:如果一个函数返回结果是一个指针,那么这个函数就是一个指针函数

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

import "fmt"

func main() {
var p = pfunc()
fmt.Println((*p)[1]) // 2
}

// 指针函数: 此处返回切片指针(用法同数组指针)
func pfunc() *[]int {
arr := []int{1, 2, 3}
return &arr
}

(4)指针参数:指针作为函数的形参

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

import "fmt"

func main() {
s := 19
argpfunc(&s)
fmt.Println(s) // 6
}

// 指针参数
func argpfunc(p *int) {
*p = 6
}

2.2.12 结构体 & 结构体方法

Go 语言中不存在 Class 类的概念,但是可以通过结构体 struct 来实现。同时在结构体中也支持指针,避免对大结构体拷贝的开销。

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
43
44
45
46
47
48
package main

import "fmt"

type user struct {
name string
password string
}

func main() {
a := user{name: "wang", password: "1024"}
b := user{"wang", "1024"}
c := user{name: "wang"}
c.password = "1024"
var d user
d.name = "wang"
d.password = "1024"
e := new(user)
e.name = "wang"
e.password = "1024"

fmt.Println(a, b, c, d) // {wang 1024} {wang 1024} {wang 1024} {wang 1024}
fmt.Println(e) // &{wang 1024}

fmt.Println(checkPassword(a, "996")) // false
changePassword(&a, "996") // 通过指针修改结构体中的数据
fmt.Println(a) // {wang 996}

// 匿名结构体 & 嵌套结构体
p := struct {
age int
sex string
u user
}{
age: 21,
sex: "Male",
u: user{"w", "1024"},
}
fmt.Println(p) // {21 Male {w 1024}}
}

func checkPassword(u user, password string) bool {
return u.password == password
}

func changePassword(u *user, password string) {
u.password = password
}

在 Golang 中还可以为结构体去定义方法,类似其他语言的类成员函数,这样就可以使用 对象.方法 去调用结构体方法了;结构体方法又分为带指针不带指针两种,不带指针的是一种拷贝。

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"

// 结构体
type user struct {
name string
password string
}

// 结构体方法
func (u user) checkingPassword(password string) bool {
return u.password == password
}

// u user: 拷贝传入的结构体, 不修改原有结构体
// u *user: 可以修改传入的结构体
func (u *user) changingPassword(password string) {
u.password = password
}

func main() {
u := user{
name: "w",
password: "1024",
}
// 结构体方法的调用
isEqual := u.checkingPassword("1024")
u.changingPassword("2022")

fmt.Println(isEqual) // true
fmt.Println(u) // {w 2022}
}

2.2.13 字符串操作

Go 语言中的 strings 标准库含有很多操作字符串的工具函数,strings 主要针对 utf-8 编码。

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

func main() {
a := "hello"
b := "你好"
fmt.Println(strings.Contains(a, "he")) // true
fmt.Println(strings.Index(a, "l")) // 2
fmt.Println(strings.Count(a, "l")) // 2
fmt.Println(strings.HasPrefix(a, "he")) // true
fmt.Println(strings.HasSuffix(a, "lo")) // true
fmt.Println(strings.Join([]string{a, b}, "-")) // hello-你好
fmt.Println(strings.Repeat(a, 2)) // hellohello
fmt.Println(strings.Replace(a, "l", "i", 1)) // heilo
fmt.Println(strings.Split("a-b-c", "-")) // [a b c]
fmt.Println(strings.ToUpper(a)) // HELLO
fmt.Println(strings.ToLower(a)) // hello
fmt.Println(len(a)) // 5
fmt.Println(len(b)) // 6
}

2.2.14 字符串格式化

😎Go 可以使用 fmt.Printf() 或者 fmt.Sprintf() 格式化字符串(后者能将格式化后的字符串赋值给新字符串)!

😎Go 语言中额外提供了占位符 %v 来打印任意类型的变量,你还可以用 %+v%#v 打印更加详细的信息 …

🔎字符串格式化符号|一览表

占位符 说明
%d 十进制的数字
%T 取类型
%s 取字符串
%t 取 bool 类型的值
%p 取内存地址
%b 整数以二进制显示
%o 整数以八进制显示
%x 整数以十六进制显示
%v 任意类型变量
%+v %v 基础上,对结构体字段名和值进行展开
%#v 输出 Go 语言语法格式的值
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
43
44
package main

import (
"fmt"
"time"
)

type point struct {
x, y int
}

func main() {
s := "hello"
n := 123
f := 3.141592653
b := true
p := point{1, 2}

// fmt.Printf()
fmt.Printf("%s\n", s) // hello
fmt.Printf("%d\n", n) // 123
fmt.Printf("%b\n", n) // 1111011
fmt.Printf("%f\n", f) // 3.141592653
fmt.Printf("%.3f\n", f) // 3.142
fmt.Printf("%t\n", b) // true

fmt.Printf("%T\n", f) // float64
fmt.Printf("%T\n", p) // main.point

fmt.Printf("s=%v\n", s) // s=hello
fmt.Printf("n=%v\n", n) // n=123
fmt.Printf("p=%v\n", p) // p={1 2}
fmt.Printf("p=%+v\n", p) // p={x:1 y:2}
fmt.Printf("p=%#v\n", p) // p=main.point{x:1, y:2}

// fmt.Sprintf()
motto := fmt.Sprintf("Today is %s, and I'm working under the %d system...\n", time.Now(), 965)
fmt.Printf(motto)

// 利用 fmt.Sprintf() 方法返回 string 的特性, 可以将 int 转为 string
intVal := 1024
var strVal string = fmt.Sprintf("%d", intVal) // int 转 string
fmt.Printf("%T", strVal) // string
}

2.2.15 接口

Golang 接口内可以定义多个方法,谁将这些方法实现,就可以认为是实现了该接口(这是一种约束,不像 Java 还需要 implements 来显式实现),这样规范了方法。在调用的时候使用不同的结构体对象,可以实现执行不同的方法。这样就实现了 Go 语言中的多态。

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
43
44
45
46
47
48
49
50
51
52
package main

import "fmt"

type action interface {
// run: run in speed
run(int)
// get: get name
get() string
}

type person struct {
name string
speed int
}

type animal struct {
types string
velocity int
}

func (per *person) run(speed int) {
fmt.Println("Run in", speed, "m/s")
}

func (per *person) get() string {
return per.name
}

func (ani *animal) run(velocity int) {
fmt.Println("Run in", velocity, "m/s")
}

func (ani *animal) get() string {
return ani.types
}

func main() {
per := person{name: "w", speed: 1}
ani := animal{types: "tiger", velocity: 6}

var act action
act = &per
act.run(per.speed) // Run in 1 m/s
name := act.get() // w

act = &ani
act.run(ani.velocity) // Run in 6 m/s
types := act.get() // tiger

fmt.Println(name, types) // w tiger
}

🌓Golang 还存在空接口 interface{},这种类型可以理解为任意类型,类似 Java 中的 Object 类。

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

import "fmt"

// T 空接口的定义, 也可以直接使用 interface{}
type T interface {
}

func test1(t T) {
fmt.Println(t)
}

// 简化: T=interface{}
func test2(t interface{}) {
fmt.Println(t)
}

⭐既然空接口可以传递任意类型,我们就可以利用这个特性把空接口 interface{} 当作容器使用。

1
2
3
4
5
6
7
8
// interface{} 作为 map 的 value
maps := make(map[int]interface{})
maps[1] = 1
maps[3] = "369"
maps[6] = true
maps[9] = 9.9

fmt.Println(maps) // map[1:1 3:369 6:true 9:9.9]
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"

// Dictionary 封装map
type Dictionary struct {
data map[string]interface{}
}

func NewDictionary() *Dictionary {
return &Dictionary{
data: make(map[string]interface{}),
}
}

func (dict *Dictionary) Set(key string, value interface{}) {
dict.data[key] = value
}

func (dict *Dictionary) Get(key string) interface{} {
return dict.data[key]
}

func main() {
// Dictionary
dict := NewDictionary()
dict.Set("a", "abandon")
dict.Set("b", 2)
dict.Set("c", false)

fmt.Println(dict.Get("a")) // abandon
fmt.Println(dict.Get("d")) // <nil>
}

💖更多关于空接口的解释:The Go Empty Interface Explained

2.2.16 错误处理

💧错误和异常不同:

  • 错误是在程序中正常存在的,可以预知的失败在意料之中。
  • 异常通常指在不应该出现问题的地方出现问题,比如空指针,这在人们的意料之外。

在 Golang 中,错误处理通常被单独作为一个返回值以传递错误信息。不同于 Java,Go 语言能很清晰的知道是哪个函数返回了错误,并且可以使用简单的 if-else 语句加以处理。

🔎error 的定义是一个接口,接口内部包含一个返回字符串类型的方法 Error()

1
2
3
type error interface {
Error() string
}

清楚 error 的定义是一个接口类型后,那么只要实现了这个接口都可以用来处理错误信息,来返回一个错误提示给用户。

Go 语言也提供了一个内置包 errors,使用 errors.New("") 来创建一个错误对象,以下为 errors 内置包中 errors.go 的定义:

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

// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error {
return &errorString{text}
}

// errorString is a trivial implementation of error.
type errorString struct {
s string
}

func (e *errorString) Error() string {
return e.s
}

⭐通常的做法是:当出错时,返回一个 nil 和一个 error;否则直接返回原有值和 nil

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

import (
"errors"
"fmt"
)

type user struct {
name string
password string
}

func findUser(users []user, name string) (v *user, err error) {
for _, u := range users {
if u.name == name {
return &u, nil
}
}
return nil, errors.New("not found")
}

func main() {
users := []user{{"w", "1024"}, {"q", "996"}}

u, e := findUser(users, "w")
if e != nil {
fmt.Println(e)
return
}
fmt.Println(*u) // {w 1024}

if us, err := findUser(users, "r"); err != nil {
fmt.Println(err) // not found
return
} else {
fmt.Println(*us)
}
}

2.2.17 时间处理

关于时间处理,最常用的莫过于 time.now() 获取当前时间。以下还有一些 time 内置包的常见用法:

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
43
44
45
46
47
package main

import (
"fmt"
"time"
)

func main() {
nowTime := time.Now()
fmt.Println(nowTime) // 2022-05-09 13:02:24.4523211 +0800 CST m=+0.007505401

// Date(): 获取年月日
year, month, day := time.Now().Date()
fmt.Println(year, month, day) // 2022 May 09
// Clock(): 获取时分秒
hour, minute, second := time.Now().Clock()
fmt.Println(hour, minute, second) // 13 1 1

// 格式化时间
formatTime1 := nowTime.Format("2006/01/02 15:04:05")
fmt.Println(formatTime1) // 2022/05/09 11:40:29
formatTime2 := nowTime.Format("2006年01月02日 15时04分05秒")
fmt.Println(formatTime2) // 2022年05月09日 11时41分25秒

// 构造带时区的时间
created := time.Date(2022, 5, 9, 11, 12, 13, 0, time.UTC)
fmt.Println(created) // 2022-05-09 11:12:13 +0000 UTC
fmt.Println(created.Year(), created.Month(), created.Day(),
created.Hour(), created.Minute(), created.Second()) // 2022 May 9 11 12 13

// Add(): 对某个时间点进行增加时间间隔的操作
// Sub(): 可以对两个时间点进行减法然后获取时间段
another := created.Add(time.Hour + time.Minute*3)
diff := another.Sub(created)
fmt.Println(diff) // 1h3m0s
fmt.Println(diff.Hours(), diff.Minutes()) // 1.05 63

// 时间戳
fmt.Println(nowTime.Unix()) // 1652417743

// 时间解析
t, err := time.Parse("2006-01-02 15:04:05", "2022-05-09 11:12:13")
if err != nil {
panic(err)
}
fmt.Println(t == created) // true
}

😮更多:

2.2.18 JSON 处理

Go 中操作 JSON 非常简单,对于一个结构体我们只需要确保每个字段的首字母为大写(即公开字段),那么这个结构体就能够用 json.Marshal 去序列化成一个 JSON byte[],如果要转化成字符串则通过 string() 即可;序列化后的字符串也可以用 json.Unmarshal 去反序列化到一个 struct 变量中。

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
43
44
45
46
47
48
49
50
package main

import (
"encoding/json"
"fmt"
)

type userInfo struct {
Name string
Age int `json:"age"` // `json:"age"` 是 json tag, 可以按 tag 名进行输出
Hobby []string
}

func main() {
// struct ==> json(string)
user := userInfo{Name: "w", Age: 21, Hobby: []string{"Java", "Golang", "Python", "C++"}}
buf, err := json.Marshal(user) // struct ==> byte[]
if err != nil {
panic(err)
}
fmt.Println(buf) // byte[]: [123 34 78 97...]
fmt.Println(string(buf)) // string: {"Name":"w","age":21,"Hobby":["Java","Golang","Python","C++"]}

// struct ==> json(string): 带有缩进的标准 JSON 格式
buf, err = json.MarshalIndent(user, "", "\t")
if err != nil {
panic(err)
}
fmt.Println(string(buf))
/* 输出结果:
{
"Name": "w",
"age": 21,
"Hobby": [
"Java",
"Golang",
"Python",
"C++"
]
}
*/

// json(string) ==> struct
var u userInfo
err = json.Unmarshal(buf, &u)
if err != nil {
panic(err)
}
fmt.Printf("%#v", u) // main.userInfo{Name:"w", Age:21, Hobby:[]string{"Java", "Golang", "Python", "C++"}}
}

2.2.19 数字解析

接下来学习下字符串与数字之间的转换,在 Go 语言中,关于字符串和数字类型的转换都在 strconv 内置包中,这个包名是 string & convert 两个单词的缩写拼接而成。

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

import (
"fmt"
"strconv"
)

func main() {
f, _ := strconv.ParseFloat("1.234", 64)
fmt.Println(f) // 1.234

n, _ := strconv.ParseInt("111", 10, 64)
fmt.Println(n) // 111

n, _ = strconv.ParseInt("0x1000", 0, 64)
fmt.Println(n) // 4096

// string ==> int
n2, _ := strconv.Atoi("123") // Atoi is equivalent to `ParseInt()`
fmt.Println(n2) // 123

// int ==> string
var str string = strconv.Itoa(123)
fmt.Println(str) // 123

n2, err := strconv.Atoi("AAA")
fmt.Println(n2, err) // 0 strconv.Atoi: parsing "AAA": invalid syntax
}

2.2.20 进程信息

在 Go 中,我们可以使用 os.Args 来获取程序执行时指定的命令行参数。

⭐比如我们编译一个二进制文件,执行 go run example/20-env/main.go a b c d 命令,其中有 a b c d 四个命令行参数,但是 os.Args 会是长度为 5 的 slice,因为第一个成员代表二进制自身的名字。

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"
"os"
"os/exec"
)

func main() {
// go run example/20-env/main.go a b c d
fmt.Println(os.Args) // [/var/folders/8p/n34xxfnx38dg8bv_x8l62t_m0000gn/T/go-build3406981276/b001/exe/main a b c d]
slices := os.Args
fmt.Println(len(slices)) // 5
fmt.Println(os.Getenv("PATH")) // /usr/local/go/bin...
fmt.Println(os.Setenv("AA", "BB"))

buf, err := exec.Command("grep", "127.0.0.1", "/etc/hosts").CombinedOutput()
if err != nil {
panic(err)
}
fmt.Println(string(buf)) // 127.0.0.1 localhost
}

🔎「Go 入门」中的部分图片引用自:https://juejin.cn/book/6844733833401597966

3. Go 实战程序

上文已经介绍了 Go 语言的基础语法和一些常用标准库的使用方法,接下来通过 3 个实例真正上手 Golang!

3.1 猜谜游戏

唯一需要注意的是 Linux/UnixWindowsMac OS 三个操作系统下换行符不一致问题!

  • Linux/Unix:换行符为 \n
  • Mac OS:换行符为 \r
  • Windows:换行符为\r\n

附部分 ASCII 码对照表

OK,如下就可以实现一个简单的猜谜游戏程序了!

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
43
44
45
46
47
48
49
50
51
52
53
package main

import (
"bufio"
"fmt"
"math/rand"
"os"
"strconv"
"strings"
"time"
)

func main() {
maxNum := 100
// 用时间戳初始化随机数种子
rand.Seed(time.Now().UnixNano())
// rand.Intn(100): 产生 0 到 100 之间的随机整数
secretNumber := rand.Intn(maxNum)

fmt.Print("Please input your guess: ")
reader := bufio.NewReader(os.Stdin)
for {
// 读取一行输入: 读到 \nxxxx\n ==> 需要去掉\n
input, err := reader.ReadString('\n') // 输入结束符: \n
if err != nil {
fmt.Println("An error occured while reading input. Please try again", err)
continue
}

// Windows —— CRLF(回车+换行): \r\n
// Linux/Unix —— LF(换行): \n
// Mac OS —— CR(回车): \r
/* strings.TrimSuffix(): 去掉最后读入的回车换行 "\r\n" */
input = strings.TrimSuffix(input, "\r\n")

// Atoi: string ==> int
// Itoa: int ==> string
guess, err := strconv.Atoi(input)
if err != nil {
fmt.Println("Invalid input. Please enter an integer value")
continue
}
fmt.Println("You guess is", guess)
if guess > secretNumber {
fmt.Println("Your guess is bigger than the secret number. Please try again")
} else if guess < secretNumber {
fmt.Println("Your guess is smaller than the secret number. Please try again")
} else {
fmt.Println("Correct, you Legend!")
break
}
}
}

3.2 在线词典

彩云小译 为例,来扒一下翻译接口:

我们需要在 Golang 中发送该请求,因为这个请求比较复杂,较难用代码构造,我们可以借助第三方工具 curlconverter 来生成代码。

首先 Copy as cURL (bash),然后借助 curlconverter 工具生成 Golang 对应的请求代码:

将代码直接运行可得到请求成功后返回的 JSON 结果(v1):

然后我们需要根据 Response Body 构造出对应的结构体,然后将响应回来的 json 字符串反序列化到结构体中,显然我们不可能自己构造(繁琐且易出错),此时需要再借助第三方工具 OKTools 生成对应的结构体。

具体做法是将彩云小译中响应的 json 字符串粘贴到 OKTools 生成对应的结构体:

构造「请求结构体」与「响应结构体」后,再次发送请求试试(v3):

可以看出所有信息都已经打印出来,但这并不是我们想要的,我们只打印 explanationsprons 这两部分的信息即可。

这样在线词典就完成了,可以运行以下程序尝试一下。

3.3 SOCKS5 代理

先浅浅演示下最终效果:启动 Golang 代理服务器程序,然后通过命令行 curl -socks5 代理服务器地址 目标URL 测试(或者通过 SwitchyOmega 插件配置,然后直接访问网站),如果代理服务器能正常工作,那么 curl 命令就会正常返回,代理服务器的日志也会打印出你所访问的网站域名或者 IP,这说明我们的网络流量是通过此代理服务器转发的。

这里我们将要编写一个较为复杂的 socks5 代理服务器,虽然 socks5 协议是代理协议,但是它并不能用于出去,它的协议使用明文传输

该协议诞生于互联网早期,因为早些时候某些互联网的内网为了确保安全性,有很严格的防火墙策略,但是这会使其访问某些资源较为麻烦,所以 socks5 应运而生,它相当于在防火墙上开个口子,让授权用户可以通过单个端口访问内部资源。

实际上很多软件最终暴露的也是一个 socks5 协议的端口,其实爬虫中所使用的 IP 代理池中很多代理协议就是 socks5

接着简单了解下 socks5 的工作原理(下附图解),大致流程是浏览器与 socks5 代理服务器建立 TCP 连接,然后 socks5 代理服务器再与目标服务器建立 TCP 连接,这里可分为四个阶段:握手阶段认证阶段请求阶段relay 阶段

  • 第一阶段——握手:浏览器向 socks5 代理服务器发起请求,其中的数据包内容包括协议版本号 VER,还有支持的认证种类 NMETHODS,以及具体的认证方法 METHOD。如果类型为 00 则表示不需要认证,如果为其他类型则进入认证流程。
  • 第二阶段——认证:不作详细介绍。
  • 第三阶段——请求:认证通过后浏览器会向 socks5 代理服务器发起 Connection 请求,主要信息包括版本号 VER、请求类型 CMD、保留字段 RSV、目标地址类型 ATYP、目标 IP & Port。代理服务器接收到请求后,会和目标服务器建立起连接,然后返回响应。
  • 第四阶段——relay:此时浏览器与目标服务器就可以通过 socks5 代理进行数据的正常收发。

SOCKS5 协议工作原理

😮在正式实现 socks5 代理前,我们先用 Golang 实现一个简单的 TCP Echo Server 过渡一下:

💖「SOCKS5 代理服务器」完整代码(附超详细的代码注释)

🥰接着就是测试环节了,命令行测试浏览器测试各演示一次。

命令行测试

浏览器测试:通过 SwitchyOmega 插件配置访问网站,代理服务器进行响应

4. 课后作业

4.1 简化猜谜游戏

关键代码:

1
2
var guess int
_, err := fmt.Scanf("%d\r\n", &guess) // windows

最终代码:

4.2 新增翻译引擎

所使用的翻译引擎:有道智云AI翻译

具体操作上文已经详细介绍过了,代码如下:

🚀这里补充推荐几个翻译接口:

⭐后三种翻译平台想要免费使用的话都需要破解,或者你可以氪金去申请对应翻译平台的 API 接口,比较稳定。

4.3 并行请求翻译

关键代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
// ...
var wg sync.WaitGroup
wg.Add(2)
go func() {
queryYouDao(word)
wg.Done()
}()
go func() {
queryCaiYun(word)
wg.Done()
}()
wg.Wait()
}

最终代码:

5. 最后

💖 如果本文对你有所帮助,点个「赞」支持一下吧!

💖/ END / 下期见!


✍️ Yikun Wu 已发表了 69 篇文章 · 总计 293k 字,采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处

🌀 本站总访问量