Go 语言指南 & 实战,跟我一起拥抱 Golang!

2,127 阅读31分钟

👨‍💻本文中的源码地址:github.com/wangkechun/…

学习目录

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

  • 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 语言基本数据类型较多,主要有以下这些:

  • 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 语句进行判断。

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 这一种循环。

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/UnixWindowsMac 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):

可以看出所有信息都已经打印出来,但这并不是我们想要的,我们只打印 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 简化猜谜游戏

关键代码:

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

最终代码:

4.2 新增翻译引擎

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

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

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

⭐后三种翻译平台想要免费使用的话都需要破解,或者你可以氪金去申请对应翻译平台的 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 / 下期见!