目前字节跳动已全面拥抱 Go 语言。除此之外,哔哩哔哩、七牛云、腾讯、百度、美团、Facebook、Google、Twitter、滴滴、深信服、知乎、去哪儿、360、微博等公司也在大量使用 Go 语言。
👨💻本文中的源码地址:https://github.com/wangkechun/go-by-example
学习目录
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
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 mainimport ( "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 mainimport ( "fmt" "math" ) var x string = "global" const ( A = iota B C = iota D = "test" E F = 9 G ) 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 j := float32 (i) k := "掘金" + a y, _ := 1 , 2 c, d = d, c const l string = "constant" const m = 10 const n = 2e3 / m fmt.Println(x, a, b, c, d, e, f, g, h, i, j, k, l, m, math.Abs(n), y) 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
语句进行判断。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package mainimport "fmt" func main () { if 5 %2 == 0 { fmt.Println("5 is even" ) } else { fmt.Println("5 is odd" ) } 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
这一种循环。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package mainimport "fmt" func main () { 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 mainimport ( "fmt" "time" ) func main () { a := 2 switch a { case 1 : fmt.Println("one" ) case 2 : fmt.Println("two" ) case 3 : fmt.Println("three" ) case 4 , 5 : fmt.Println("four or five" ) default : fmt.Println("other" ) } 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 mainimport "fmt" func main () { var a [5 ]int a[4 ] = 100 fmt.Println("get:" , a[2 ]) fmt.Println("len:" , len (a)) b := [3 ]int {1 , 2 , 3 } fmt.Println(b) c := [...]int {1 , 3 , 2 } fmt.Println(c) d := [...]int {1 : 3 , 6 : 5 } fmt.Println(d) 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) }
🚫不过在真实的业务场景中,我们很少直接使用定长的数组,更多使用的是切片。
2.2.7 切片 slice 数组是定长的,所以 Go 推出可扩容的切片。与数组不同的是,切片不需要指定 []
里的长度。
1 2 3 4 5 6 7 8 9 10 var slice1 []string var slice2 = make ([]int , 3 )slice3 := []string {"g" , "o" , "o" , "d" } slice4 := make ([]int , 3 ) 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 mainimport "fmt" func main () { s := make ([]string , 3 , 5 ) s[0 ] = "a" s[1 ] = "b" s[2 ] = "c" fmt.Println("get:" , s[2 ]) fmt.Println("len:" , len (s)) fmt.Println("cap:" , cap (s)) s = append (s, "d" ) s = append (s, "e" , "f" ) fmt.Println(s) c := make ([]string , len (s)) copy (c, s) fmt.Println(c) cs := []string {"w" , "w" , "w" , "w" , "w" } copy (cs, s[:3 ]) fmt.Println(cs) fmt.Println(s[2 :5 ]) fmt.Println(s[:5 ]) fmt.Println(s[2 :]) good := []string {"g" , "o" , "o" , "d" } fmt.Println(good) good = append (good, c...) fmt.Println(good) }
🤔切片元素的删除 :Go 语言中并没有提供一个内置函数将切片中的元素进行删除,但我们可以使用 [x,y]
或者 append
来实现。
1 2 3 4 5 6 7 slice := []int {1 , 2 , 3 , 4 , 5 } slice = slice[1 :] idx := 1 slice = append (slice[:idx], slice[idx+1 :]...) fmt.Println(slice)
⭐小结一下:
每一个切片都引用了一个底层数组。
切片创建时存储了一个长度和一个容量,还有一个指向数组的指针,当切片添加数据时,如果没有超过容量,直接添加,超出容量自动扩容成倍增长。
一旦切片扩容,指针会指向一个新的底层数组内存地址。
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 var map1 map [int ]string map2 := make (map [int ]string ) 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 mainimport "fmt" func main () { m := make (map [string ]int ) m["one" ] = 1 m["two" ] = 2 fmt.Println(m) fmt.Println(len (m)) fmt.Println(m["one" ]) fmt.Println(m["unknow" ]) var nilMap map [int ]float32 if nilMap == nil { nilMap = make (map [int ]float32 ) } nilMap[0 ] = 1.0 fmt.Println(nilMap) r, ok := m["unknow" ] fmt.Println(r, ok) t, ok := m["two" ] fmt.Println(t, ok) delete (m, "one" ) delete (m, "two" ) fmt.Println(m) m2 := map [string ]int {"one" : 1 , "two" : 2 } var m3 = map [string ]int {"one" : 1 , "two" : 2 } fmt.Println(m2, m3) }
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 mainimport ( "fmt" "strconv" ) func main () { nums := []int {2 , 3 , 4 } sum := 0 for i, num := range nums { sum += num if num == 2 { fmt.Println("index:" , i, "num:" , num) } } fmt.Println(sum) for _, num := range nums { fmt.Println(num) } maps := make (map [int ]string ) maps[0 ] = "hello" maps[1 ] = "world" for key, value := range maps { fmt.Println(fmt.Sprintf("%d" , key) + ":" + value) fmt.Println(strconv.Itoa(key) + ":" + value) 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 mainimport "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) v, ok := exists(map [string ]string {"a" : "A" }, "a" ) fmt.Println(v, ok) }
💖Go 语言中的函数也是一种数据结构,也可以被存储在一个变量中,调用变量的时候也就相当于调用函数——也就是可以将函数作为传递参数 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package mainimport "fmt" func add (a int , b int ) (int , string ) { return a + b, "ok" } func main () { f := add ret, _ := f(2 , 3 ) fmt.Println(ret) var addingFunc func (int , int ) (int , string ) addingFunc = add result, str := addingFunc(1 , 5 ) fmt.Println(result, str) }
💖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 mainimport ( "fmt" "strconv" ) func main () { func () { fmt.Println("匿名函数" ) }() sum := func (a int , b int ) int { return a + b }(1 , 2 ) fmt.Println(sum) myFunc := func (a, b int ) string { return strconv.Itoa(a) + fmt.Sprintf("%d" , b) } returnVal := myFunc(6 , 66 ) fmt.Println(returnVal) }
💖甚至你可以将匿名函数作为另一个函数的参数/返回值;其中作为参数的函数叫做回调函数 ,调用的函数叫做高阶函数 。
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 mainimport "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 } 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) num2 := opera(1 , 2 , reduce) fmt.Println(num1, num2) num3 := opera(3 , 4 , func (a int , b int ) int { return a * b }) fmt.Println(num3) }
💖既然 Go 语言中的函数能作为返回值和参数,自然能打造闭包结构 ,与 JS 闭包含义相同。
所谓闭包,就是一个外层函数中有内层函数,这个内层函数会操作外层函数的局部变量,并且,外层函数把内层函数作为返回值,将内层函数与外层函数中的局部变量统称为闭包结构。
💖先捋清下为什么我们需要闭包结构,闭包有什么作用?
首先看一个简单的计数器例子!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package mainimport "fmt" var counter = 0 func add () int { counter++ return counter } func main () { add() add() add() fmt.Println(counter) }
虽然我们已经达到了目的,但是任意一个函数中都可以随意改动 counter
的值,所以该计数器并不完美,那我们将 counter
放到函数中如何?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package mainimport "fmt" func add () int { counter := 0 counter++ return counter } func main () { add() add() ret := add() fmt.Println(ret) }
本意想输出 3,但由于局部变量在函数每次调用时都会被初始化为 0,所以达不到预期效果。所以我们此时就需要使用闭包 来解决了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package mainimport "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) }
精简下闭包代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package mainimport "fmt" func main () { add := func () func () int { counter := 0 return func () int { counter++ return counter } }() add() add() ret := add() fmt.Println(ret) }
💖现在估计你能很轻松的理解以下代码了。
注意:由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。因此可以手动解除对内层匿名函数的引用,以便释放内存。
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 mainimport "fmt" func main () { res := closure() r1 := res() r2 := res() fmt.Println(res) fmt.Println(r1) fmt.Println(r2) res = nil } func closure () func () int { 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 mainimport "fmt" func main () { defer func () { fmt.Println("Close Resource" ) }() fmt.Println("defer..." ) }
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 mainimport "fmt" func add2 (n int ) { n += 2 } func add2ptr (n *int ) { *n += 2 } func main () { n := 5 add2(n) fmt.Println(n) add2ptr(&n) fmt.Println(n) }
🚀接着简单介绍下:「数组指针 」、「指针数组 」、「指针函数 」、「指针参数 」
区分数组指针 和指针数组 的定义,只需看清变量名更靠近 []
(指针数组) 还是 *
(数组指针)。
至于要区分这二者的使用方式只需要记得 []
优先级高于 *
即可。
(1)数组指针:指向数组的指针
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package mainimport "fmt" func main () { arr := [3 ]int {1 , 2 , 3 } var ars *[3 ]int ars = &arr (*ars)[0 ] = 0 ars[1 ] = 1 fmt.Println(ars) fmt.Println(*ars) fmt.Println((*ars)[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) fmt.Println(numps) fmt.Println(*numps[0 ]) fmt.Println(*numps[2 ]) for _, v := range numps { fmt.Print(*v, " " ) }
(3)指针函数:如果一个函数返回结果是一个指针,那么这个函数就是一个指针函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package mainimport "fmt" func main () { var p = pfunc() fmt.Println((*p)[1 ]) } 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 mainimport "fmt" func main () { s := 19 argpfunc(&s) fmt.Println(s) } 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 mainimport "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) fmt.Println(e) fmt.Println(checkPassword(a, "996" )) changePassword(&a, "996" ) fmt.Println(a) p := struct { age int sex string u user }{ age: 21 , sex: "Male" , u: user{"w" , "1024" }, } fmt.Println(p) } 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 mainimport "fmt" type user struct { name string password string } func (u user) checkingPassword(password string ) bool { return u.password == password } 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) fmt.Println(u) }
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 mainimport ( "fmt" "strings" ) func main () { a := "hello" b := "你好" fmt.Println(strings.Contains(a, "he" )) fmt.Println(strings.Index(a, "l" )) fmt.Println(strings.Count(a, "l" )) fmt.Println(strings.HasPrefix(a, "he" )) fmt.Println(strings.HasSuffix(a, "lo" )) fmt.Println(strings.Join([]string {a, b}, "-" )) fmt.Println(strings.Repeat(a, 2 )) fmt.Println(strings.Replace(a, "l" , "i" , 1 )) fmt.Println(strings.Split("a-b-c" , "-" )) fmt.Println(strings.ToUpper(a)) fmt.Println(strings.ToLower(a)) fmt.Println(len (a)) fmt.Println(len (b)) }
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 mainimport ( "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("%s\n" , s) fmt.Printf("%d\n" , n) fmt.Printf("%b\n" , n) fmt.Printf("%f\n" , f) fmt.Printf("%.3f\n" , f) fmt.Printf("%t\n" , b) fmt.Printf("%T\n" , f) fmt.Printf("%T\n" , p) fmt.Printf("s=%v\n" , s) fmt.Printf("n=%v\n" , n) fmt.Printf("p=%v\n" , p) fmt.Printf("p=%+v\n" , p) fmt.Printf("p=%#v\n" , p) motto := fmt.Sprintf("Today is %s, and I'm working under the %d system...\n" , time.Now(), 965 ) fmt.Printf(motto) intVal := 1024 var strVal string = fmt.Sprintf("%d" , intVal) fmt.Printf("%T" , strVal) }
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 mainimport "fmt" type action interface { run(int ) 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) name := act.get() act = &ani act.run(ani.velocity) types := act.get() fmt.Println(name, types) }
🌓Golang 还存在空接口 interface{}
,这种类型可以理解为任意类型,类似 Java 中的 Object
类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package mainimport "fmt" type T interface {} func test1 (t T) { fmt.Println(t) } func test2 (t interface {}) { fmt.Println(t) }
⭐既然空接口可以传递任意类型,我们就可以利用这个特性把空接口 interface{}
当作容器使用。
1 2 3 4 5 6 7 8 maps := make (map [int ]interface {}) maps[1 ] = 1 maps[3 ] = "369" maps[6 ] = true maps[9 ] = 9.9 fmt.Println(maps)
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 mainimport "fmt" 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 () { dict := NewDictionary() dict.Set("a" , "abandon" ) dict.Set("b" , 2 ) dict.Set("c" , false ) fmt.Println(dict.Get("a" )) fmt.Println(dict.Get("d" )) }
💖更多关于空接口的解释: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 errorsfunc New (text string ) error { return &errorString{text} } 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 mainimport ( "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) if us, err := findUser(users, "r" ); err != nil { fmt.Println(err) 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 mainimport ( "fmt" "time" ) func main () { nowTime := time.Now() fmt.Println(nowTime) year, month, day := time.Now().Date() fmt.Println(year, month, day) hour, minute, second := time.Now().Clock() fmt.Println(hour, minute, second) formatTime1 := nowTime.Format("2006/01/02 15:04:05" ) fmt.Println(formatTime1) formatTime2 := nowTime.Format("2006年01月02日 15时04分05秒" ) fmt.Println(formatTime2) created := time.Date(2022 , 5 , 9 , 11 , 12 , 13 , 0 , time.UTC) fmt.Println(created) fmt.Println(created.Year(), created.Month(), created.Day(), created.Hour(), created.Minute(), created.Second()) another := created.Add(time.Hour + time.Minute*3 ) diff := another.Sub(created) fmt.Println(diff) fmt.Println(diff.Hours(), diff.Minutes()) fmt.Println(nowTime.Unix()) 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) }
😮更多:
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 mainimport ( "encoding/json" "fmt" ) type userInfo struct { Name string Age int `json:"age"` Hobby []string } func main () { user := userInfo{Name: "w" , Age: 21 , Hobby: []string {"Java" , "Golang" , "Python" , "C++" }} buf, err := json.Marshal(user) if err != nil { panic (err) } fmt.Println(buf) fmt.Println(string (buf)) buf, err = json.MarshalIndent(user, "" , "\t" ) if err != nil { panic (err) } fmt.Println(string (buf)) var u userInfo err = json.Unmarshal(buf, &u) if err != nil { panic (err) } fmt.Printf("%#v" , u) }
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 mainimport ( "fmt" "strconv" ) func main () { f, _ := strconv.ParseFloat("1.234" , 64 ) fmt.Println(f) n, _ := strconv.ParseInt("111" , 10 , 64 ) fmt.Println(n) n, _ = strconv.ParseInt("0x1000" , 0 , 64 ) fmt.Println(n) n2, _ := strconv.Atoi("123" ) fmt.Println(n2) var str string = strconv.Itoa(123 ) fmt.Println(str) n2, err := strconv.Atoi("AAA" ) fmt.Println(n2, err) }
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 mainimport ( "fmt" "os" "os/exec" ) func main () { fmt.Println(os.Args) slices := os.Args fmt.Println(len (slices)) fmt.Println(os.Getenv("PATH" )) 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)) }
🔎「Go 入门」中的部分图片引用自:https://juejin.cn/book/6844733833401597966
3. Go 实战程序 上文已经介绍了 Go 语言的基础语法和一些常用标准库的使用方法,接下来通过 3 个实例真正上手 Golang!
3.1 猜谜游戏
唯一需要注意的是 Linux/Unix 、Windows 、Mac 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 mainimport ( "bufio" "fmt" "math/rand" "os" "strconv" "strings" "time" ) func main () { maxNum := 100 rand.Seed(time.Now().UnixNano()) secretNumber := rand.Intn(maxNum) fmt.Print("Please input your guess: " ) reader := bufio.NewReader(os.Stdin) for { input, err := reader.ReadString('\n' ) if err != nil { fmt.Println("An error occured while reading input. Please try again" , err) continue } input = strings.TrimSuffix(input, "\r\n" ) 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 简化猜谜游戏 关键代码:
1 2 var guess int _, err := fmt.Scanf("%d\r\n" , &guess)
最终代码:
4.2 新增翻译引擎 所使用的翻译引擎:有道智云AI翻译
具体操作上文已经详细介绍过了,代码如下:
🚀这里补充推荐几个翻译接口:
必应翻译 :Level 1
火山翻译 :Level 1
有道翻译 :Level 2(接口被加密,没法轻易破解)
salt
随机数:时间 + rand 生成
sign
:md5 加密认证
谷歌翻译 :Level 3
百度翻译 :Level 3
⭐后三种翻译平台想要免费使用的话都需要破解,或者你可以氪金去申请对应翻译平台的 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 / 下期见!