👨💻本文中的源码地址:github.com/wangkechun/…
学习目录
1. Go 简介
介绍下 Go 语言的特性以及目前 Go 语言在市场上的 "帝位"。
1.1 Go 语言特性
- 高性能 & 高并发:Go 语言天生支持高并发(goroutine & channel & 调度器),不像很多语言通过库的形式支持;Go 像一些低级别的语言 (C/C++) 一样是一门编译型语言,这意味着它的性能足以媲美 C/C++!
- 语法简洁易懂:语法类似 C 语言,比如:同时去掉了不需要的
()
,循环也只简化成for
循环这一种表示... 该特点使得用 Go 编写的代码易于维护。 - 丰富的标准库:Go 语言带有极其丰富与完善的标准库,无需再借助第三方库完成便可以应对日常的开发,而且能持续享受到语言迭代带来的性能优化。
- 完整的工具链:编译、代码格式化、错误检查、包管理、代码补全等都有相应的工具,Go 语言还内置了单元测试框架(单元测试、性能测试、代码覆盖率、性能优化)。
- 静态链接:所有的编译结果默认都是静态链接的,只需要拷贝编译后唯一的可执行文件即可部署运行,线上发布的体积可以控制得很小。
- 快速编译:Go 语言一开始设计就考虑到快速编译。它能像其他解释型语言一样(Python & JavaScript),你不会注意它正在编译。
- 跨平台:Go 语言能在 Linux、MacOS、Windows 等操作系统下运行,还能用于开发 Android、iOS 软件,甚至能运行在路由器、树莓派等等设备;同时具备交叉编译特性。
- 垃圾回收: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
- go.dev/ : Golang 官网
- studygolang.com/dl : Golang 中国镜像(Golang 官网无法打开的情况可用)
- goproxy.cn/ : 七牛云 Go 模块代理,配置
go mod proxy
并按网站提示进行操作可助力提升下载第三方包的速度(优化访问 GitHub 较慢的情况)
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 程序的运行
package main
import (
"fmt"
)
func main() {
fmt.Println("hello world")
}
有 2 种方式运行该段程序:
go run
命令可直接运行程序go build
可将程序编译成二进制,编译完成后直接执行./main
即可运行
2.2.1 变量 & 常量
Golang 是一门强类型语言(每个变量都有其对应的类型),变量 var
如果没有标注类型会自动推导,常量 const
默认没有确定类型(自动推导)。
注意:如果定义了变量,必须得使用,否则编译不通过
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 语言基本数据类型较多,主要有以下这些:
byte
、int
、int8
、int16
、int32
、int64
、uint
...float32
、float64
error
string
bool
rune
🌓Go 语言囊括的复合数据类型范围较广 (下文慢慢介绍) :
- 数组
array
- 切片
slice
- 字典
map
- 函数
func
- 结构体
struct
- 通道
channel
- 接口
interface
- 指针
*
🚀根据数据特点又可分为值传递 & 引用传递:
- 值传递:
int
、float
、string
、bool
、array
、struct
- 引用传递:
slice
、map
、chan
、*
⭐至于运算符(算法运算符、关系运算符、逻辑运算符、位运算符、赋值运算符)和其他语言基本一致,这里不再赘叙。
2.2.3 if-else
条件判断
Go 语言中的 if
判断语句不需要 ()
;同理,switch
和 for
也不需要。
Go 语言中的 if
语句存在一种特殊的写法:
err
是myFunc()
的返回值,执行后再对err==nil
语句进行判断。
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 语言中没有 while
、do while
这类循环,仅仅只有 for
这一种循环。
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
条件判断结构了。
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
数组是一个长度固定的元素序列,可利用索引取值/存值。
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.声明空切片
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]
来获取切片中指定范围的值。
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
来实现。
// 切片元素的删除
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
:
// 声明空 map: 不可直接使用
var map1 map[int]string
// 创建 map (已初始化——零值填充): 可直接使用
map2 := make(map[int]string)
// 声明并初始化 map: 可直接使用
map3 := map[string]int{"one": 1, "two": 2}
使用 map
:
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:第一个是键,第二个是值
如果不需要索引/键,可以直接使用 _
匿名变量代替。
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 中的函数原生支持返回多个值,且在实际业务场景中都返回两个值,第一个是真正的返回结果,第二个是错误信息。
多返回值
接下来看看函数的基本定义及其用法:
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 语言中的函数也是一种数据结构,也可以被存储在一个变量中,调用变量的时候也就相当于调用函数——也就是可以将函数作为传递参数。
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 中定义匿名函数加上 ()
相当于直接调用;如果没有 ()
则表示定义一个函数,可将其赋值给变量然后进行多次调用——匿名函数。
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
}
💖甚至你可以将匿名函数作为另一个函数的参数/返回值;其中作为参数的函数叫做回调函数,调用的函数叫做高阶函数。
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 闭包含义相同。
所谓闭包,就是一个外层函数中有内层函数,这个内层函数会操作外层函数的局部变量,并且,外层函数把内层函数作为返回值,将内层函数与外层函数中的局部变量统称为闭包结构。
💖先捋清下为什么我们需要闭包结构,闭包有什么作用?
首先看一个简单的计数器例子!
package main
import "fmt"
var counter = 0
func add() int {
counter++
return counter
}
func main() {
add()
add()
add()
fmt.Println(counter) // 3
}
虽然我们已经达到了目的,但是任意一个函数中都可以随意改动 counter
的值,所以该计数器并不完美,那我们将 counter
放到函数中如何?
package main
import "fmt"
func add() int {
counter := 0
counter++
return counter
}
func main() {
add()
add()
ret := add()
fmt.Println(ret) // 1
}
本意想输出 3,但由于局部变量在函数每次调用时都会被初始化为 0,所以达不到预期效果。所以我们此时就需要使用闭包来解决了。
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
}
精简下闭包代码:
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
}
💖现在估计你能很轻松的理解以下代码了。
注意:由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。因此可以手动解除对内层匿名函数的引用,以便释放内存。
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
执行结束后才会执行;一般用于资源的关闭。
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 语言中通过 &
获取变量的地址,通过 *
获取指针所对应的变量存储的数值。
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)数组指针:指向数组的指针
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)指针数组:数组元素皆为指针
// 指针数组
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)指针函数:如果一个函数返回结果是一个指针,那么这个函数就是一个指针函数
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)指针参数:指针作为函数的形参
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
来实现。同时在结构体中也支持指针,避免对大结构体拷贝的开销。
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 中还可以为结构体去定义方法,类似其他语言的类成员函数,这样就可以使用 对象.方法
去调用结构体方法了;结构体方法又分为带指针和不带指针两种,不带指针的是一种拷贝。
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
编码。
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 语言语法格式的值 |
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 语言中的多态。
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
类。
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{}
当作容器使用。
// 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]
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()
。
type error interface {
Error() string
}
清楚 error
的定义是一个接口类型后,那么只要实现了这个接口都可以用来处理错误信息,来返回一个错误提示给用户。
Go 语言也提供了一个内置包 errors
,使用 errors.New("")
来创建一个错误对象,以下为 errors
内置包中 errors.go
的定义:
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
。
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
内置包的常见用法:
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
变量中。
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 两个单词的缩写拼接而成。
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,因为第一个成员代表二进制自身的名字。
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 入门」中的部分图片引用自:juejin.cn/book/684473…
3. Go 实战程序
上文已经介绍了 Go 语言的基础语法和一些常用标准库的使用方法,接下来通过 3 个实例真正上手 Golang!
3.1 猜谜游戏
唯一需要注意的是 Linux/Unix、Windows、Mac OS 三个操作系统下换行符不一致问题!
- Linux/Unix:换行符为
\n
- Mac OS:换行符为
\r
- Windows:换行符为
\r\n
附部分 ASCII 码对照表
OK,如下就可以实现一个简单的猜谜游戏程序了!
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
):
可以看出所有信息都已经打印出来,但这并不是我们想要的,我们只打印 explanations
和 prons
这两部分的信息即可。
这样在线词典就完成了,可以运行以下程序尝试一下。
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 简化猜谜游戏
关键代码:
var guess int
_, err := fmt.Scanf("%d\r\n", &guess) // windows
最终代码:
4.2 新增翻译引擎
所使用的翻译引擎:有道智云AI翻译
具体操作上文已经详细介绍过了,代码如下:
🚀这里补充推荐几个翻译接口:
- 必应翻译:Level 1
- 火山翻译:Level 1
- 有道翻译:Level 2(接口被加密,没法轻易破解)
salt
随机数:时间 + rand 生成sign
:md5 加密认证
- 谷歌翻译:Level 3
- 百度翻译:Level 3
⭐后三种翻译平台想要免费使用的话都需要破解,或者你可以氪金去申请对应翻译平台的 API 接口,比较稳定。
4.3 并行请求翻译
关键代码:
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 / 下期见!