Go语言圣经-Notes

书中的示例代码仓库

Go 官方网站

Go 官方博客网站

Go Playground 地址

1 入门

获取命令行参数

存储在 os.Args 变量中, 其为一个字符串切片.

同样, os.Args[0] 保存当前命令本身的名字 (不是文件名).

循环

Go 语言只有 for 循环这一种循环语句, 而有多种形式.

range 函数

如:

1
2
3
for index, value := range collection {
// 执行操作
}

其中,collection 可以是数组、切片、字符串、映射或通道,index 是当前元素的索引(或键),value 是当前元素的值。

strings.Join

用于将一个切片进行字符串拼接.

如:

1
var s string = strings.Join(os.Args, " ")

1.3 查找重复的行

bufio

主要有三种类型:

  • Reader
  • Writer
  • Scanner

创建的语法都如:

1
input := bufio.NewScanner(os.Stdin)

类型的 Text() 方法可以获取读取的字符串:

1
2
3
for input.Scan() {
fmt.Println(input.Text())
}

Scan 函数在读到一行时返回 true, 不再有输入时返回 false, 且可以设置分隔符.

os.Stdinos.Open 返回的描述符的类型

都是 *os.File 类型指针.

向函数传入 map 类型数据

其传递的是 map 的引用 (可能本身就是指针).

io/ioutil

其中的 ReadFile 函数用于读取指定文件的全部内容. 返回值是字符串切片.

strings.Split 函数

将字符串分割成字串的切片

1.5 获取 URL

使用 net 之下的包.

net/http

http.Get 用于发出 get 请求. 其会返回一个结构体:

1
resp, err := http.Get(url)

读取响应流内容:

1
b, err := ioutil.ReadAll(resp.Body)

关闭响应流:

1
resp.Body.Close()

获取状态码:

1
resp.Status

io.Copy

如:

1
io.Copy(dst, src)

其会从 src 中读取内容, 并将结果写入到 dst 中.

注意 dstio.Writer 对象.

strings.HasPrefix

其函数声明为:

1
func HasPrefix(s string, prefix string) bool

检查一个字符串是否有前缀 prefix.

1.6 并发获取多个 URL

time 标准库

time.Now() 返回本地时间.

获取相隔的时间:

1
2
start := time.Now()
time.Since(start).Seconds()

ioutil.Discard

不要的数据可以输出到这里, 如:

1
io.Copy(ioutil.Discard, input)

1.7 Web 服务

同样用 net/http 标准库.

示例如:

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

import (
"fmt"
"log"
"net/http"
)

func main() {
http.HandleFunc("/", handler) // each request calls handler
log.Fatal(http.ListenAndServe("localhost:8000", nil))
}

// handler echoes the Path component of the requested URL.
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
}

1.8 本章重点

switchelse if 用法:

1
2
3
4
5
6
7
8
switch {
case x > 0:
return +1
default:
return 0
case x < 0:
return -1
}

这种形式被称为无 tag switch.

2 程序结构

2.3 变量

常见零值:

  • 数值类型: 0
  • 布尔类型: false
  • 字符串类型: '' (空字符串)
  • 接口或引用类型(包括 slice, 指针, map, chan 和函数):nil
  • 数组或结构体类型: 对应元素的零值

2.3.1 简短变量声明

当简短变量声明左边有变量在相同的词法域内声明过了, 则对其只有赋值行为.

需要确保简短变量声明语句必须至少声明一个新的变量.

2.3.2 指针

在 Go 语言中, 返回函数中局部变量的地址也是安全的.

flag 标准库

  • flag.Bool, 创建布尔型标志参数的变量
  • flag.String, 创建字符串类型标志参数的变量
  • flag.Args(), 访问普通命令行参数
  • flag.Parse, 更新标志参数对应的值

示例:

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

import (
"flag"
"fmt"
"strings"
)

var n = flag.Bool("n", false, "omit trailing newline")
var sep = flag.String("s", " ", "separator")

func main() {
flag.Parse()
fmt.Print(strings.Join(flag.Args(), *sep))
if !*n {
fmt.Println()
}
}

2.3.3 new 函数

new(T) 创建一个 T 类型的匿名变量, 初始化为 T 类型的零值, 然后返回变量地址, 返回的指针类型为 *T

1
2
p := new (int)
fmt.Println(*p)

2.3.4 变量的生命周期

一个变量的有效周期只取决于是否可达.

编译器会自动选择在栈上还是在堆上分配局部变量的存储空间.

2.5 类型

对于每一个类型 T, 都有一个对应的类型转换操作 T(x), 用于将 x 转为 T 类型.

2.6 包和文件

包级别的名字, 在同一个包的其他源文件也是可以直接访问的.

每个源文件的包声明前紧跟着的注释是 包注释 , 通常, 包注释的第一句应该是包的功能概要说明, 一个包通常只有一个源文件有包注释.

2.6.1 包的导入

golang.org/x/tools/cmd/goimports 导入工具, 可以根据需要自动添加或删除导入的包.

2.6.2 包的初始化

包级变量的初始化会按照依赖顺序:

1
2
3
4
5
var a = b + c // a 第三个初始化, 为3
var b = f() // b 第二个初始化, 为 2
var c = 1 // c 第一个初始化, 为 1

func f() int { return c + 1 }

对于一个包中的多个 .go 源文件, Go 语言的构建工具首先会将 .go 文件根据文件名排序, 然后依次调用编译器编译.

每个包在解决依赖的前提下, 以导入声明的顺序初始化, 每个包只会被初始化一次.

main 包是最后初始化的.

2.7 作用域

不能混淆作用域和生命周期.

一个隐式的词法域 – for 的初始化部分:

1
2
3
4
5
6
x := "hello"

for _, x := range x {
x := x + 'A' - 'a'
fmt.Printf("%c", x)
}

这里声明了三个不同的 x.

这种隐式词法域也出现在 if, switch 等语句.

在包级别, 声明的顺序并不会影响作用域范围.

3 基础数据类型

Go 语言将数据类型分为四类:

  • 基础类型, 数字, 字符串和布尔
  • 复合类型, 数组和结构体
  • 引用类型, 切片, 指针, 字典, 函数, 通道
  • 接口类型

3.4 布尔型

用于转换的简单函数:

1
2
3
4
5
6
7
8
9
10
// btoi returns 1 if b is true and 0 if false
func btoi(b bool) int {
if b {
return 1
}
return 0
}

// itob reports whether i is non-zero
func itob(i int) bool { return i != 0 }

3.5 字符串

内置的 len 函数可以返回一个字符串中的字节数目, 索引操作 s[i] 返回第 i 个字节 (注意是字节而不是字符) 的字节值:

1
2
3
s := "hello world"
fmt.Println( len(s) )
fmt.Println(s[0], s[7])

3.5.1 字符串面值

原生的字符串面值用反引号包裹 (不会有转义操作).

3.5.4 字符串和 Byte 切片

标准库中有四个包对字符串处理比较重要:

  • bytes
  • strings
  • strconv
  • unicode

strings.LastIndex

返回一个字符在一个字符串中最后一次出现的索引:

1
2
s := "/home/test"
index := strings.LastIndex(s, "/")

这里 index 的值为 5

3.5.5 字符串和数字的转换

strconv 提供可用函数.

3.6 常量

常量表达式的值在编译期计算, 而不是在运行期.

每种常量的潜在类型都是基础类型: boolean, string 或数字.

一个常量的声明也可以包含一个类型和一个值, 如果没有显示指明类型, 则从右边的表达式推断.

3.6.1 iota 常量生成器

其用于生成一组以相似规则初始化的常量, 但是不用每行都写一遍初始化表达式. 在一个 const 声明语句中, 在第一个声明的常量所在的行, iota 将会被置为 0, 然后在每一个有常量声明的行加一。

如:

1
2
3
4
5
6
7
8
9
10
11
12
type Weekday int

const (
Sunday Weekday = iota
Monday
Tueday
Wednesday
Thursday
Friday
Saturday
)

3.6.2 无类型常量

声明常量时不指定类型可以获得更高的精度, 且可以直接用于更多的表达式而不需要显式的类型转换.

可以分为 6 种类型:

  • 无类型布尔型
  • 无类型整数
  • 无类型字符
  • 无类型浮点数
  • 无类型复数
  • 无类型字符串

对于一个没有显示类型的变量声明, 常量的形式将隐式决定变量的默认类型, 如:

1
2
3
4
i := 0      // untyped integer; implicit int(0)
r := '\000' // untyped rune; implicit rune('\000')
f := 0.0 // untyped floating-point; implicit float64(0.0)
c := 0i // untyped complex; implicit complex128(0i)

若要给变量一个不同的类型, 需显示地将无类型的常量转化为所需的类型, 如:

1
2
var i = int8 (0)
var i int8 = 0

4 复合数据类型

数组和结构体都是有固定内存大小的数据结构, 而 slicemap 则是动态的数据结构.

  • 每个数组元素都是完全相同的类型
  • 结构体是由异构的元素组成

4.1 数组

注意 [3]int4[int] 是两种不同的数组类型.

数组的长度必须是常量表达式, 其在编译阶段确定.

注意, 在 Go 语言中, 数组不会被隐式当作指针. 因此传递给函数时需要显示传入一个数组指针. 如:

1
func zero(ptr *[32]byte)

4.2 切片

一个 slice 由三个部分构成:

  • 指针
  • 长度
  • 容量

其底层是引用一个数组对象.

指针指向第一个 slice 元素对应的底层数组元素的地址. 如:

1
2
arr := []string{"he", "llo", "wor", "ld"}
fmt.Printf("%p %p", arr, &arr[0])

打印的值相同.

长度不能超过容量. 容量一般从 slice 的开始位置到底层数据的结尾位置.

从一个数组获取的多个切片可以共享底层的数据.

如:

1
2
3
4
5
arr := [...]string{"he", "llo", "wor", "ld"}
slice1 := arr[0:3]
slice2 := arr[1:2]
slice2[0] = "Test suc"
fmt.Print(slice1[1])

输出 Test suc.

直接声明一个切片数组:

1
s := []int {0, 1, 2, 3, 4, 5}

其会隐式创建一个合适大小的数组, 然后 slice 的指针指向底层的数组.

注意 , 和数组不同, slice 不能直接用 == 比较, 因为 slice 的元素是间接引用的 (一个固定的 slice 值在不同时刻可能包含不同的元素)

可以用标准库的 bytes.Equal 函数来判断.

7 接口

7.5 接口值

接口值 (interface value) 是指接口类型的变量在运行时持有的实际值. 接口值由两部分组成: 动态类型 (dynamic type) 和动态值 (dynamic value).

对于一个接口的零值就是它的类型和值的部分都是 nil:

10 包和工具

查看标准包:

1
go list std | wc -l

为了避免冲突, 所有非标准库包的导入路径建议以所在组织的互联网域名为前缀.

如使用 bubbletea 这个包, 引入方式则为:

1
2
3
4
5
6
7
8
package main

import (
"fmt"
"os"

tea "github.com/charmbracelet/bubbletea"
)

这个仓库的位置就是 https://github.com/charmbracelet/bubbletea. (这里 tea 是别名)

注意, 用 import 引入的都是 package name, 而不是文件名. 似乎每一个目录都只会含有一个包.

_test.go 为后缀的源文件, 且以 _test 为后缀的包名, 都是由 go test 独立编译的外部测试文件.

导入两个具有相同包名的包的方法:

1
2
3
4
import (
"crypto/rand"
mrand "math/rand"
)

(给一个取新的包名)

包的匿名导入

导入一个包而不使用

1
import _ "image/png"

(也叫匿名导入)

工具

GOPATH 环境变量, 用于指定当前工作目录. 这个目录下会被创建三个子目录:

  • src, 存储源代码
  • pkg, 保存编译后的包的目标文件
  • bin, 保存编译后的可执行程序

GOROOT 环境变量, 指定 Go 的安装目录, 带标准库的位置. 目录结构与 GOPATH 下一致.

可以用 go env 查看所有相关环境变量.

包文档

如果一个注释之后紧跟着包声明语句, 那注释则为整个包的文档.

打开包/成员/方法的文档:

1
2
3
go doc time
go doc time.Since
go doc time.Duration.Seconds

内部包

若一个包的导入路径包含 internal 名称, 其会被做一些特殊处理: 只能被和 internal 目录有同一个父目录的包所导入.

比如 net/http/internal/chunked 只能被:

  • net/http
  • net/http/httputil
    导入.

11 测试

Go 以来 go test 测试命令以及一组按照约定方式编写的测试函数进行测试.

go test

在包目录内, 所有以 _test.go 为后缀名的源文件在执行 go build 时不会被构建成包的一部分. (其属于 go test 测试的一部分)

*_test.go 文件中有三种函数:

  • 测试函数, 以 Test 为函数名前缀的函数
  • 基准测试函数, 以 Benchmark 为函数名前缀的函数
  • 示例函数, 以 Example 为函数名前缀的函数

go test 命令会遍历所有的 *_test.go 文件中符合上述命名规则的函数, 生成一个临时的 main 包用于调用相应的测试函数, 接着构建并运行, 报告测试结果, 清除测试中生成的临时文件.

测试函数

如:

1
2
3
4
5
import "testing"

func TestName(t *testing.T) {
// ...
}

注意必须导入 testing 包, t 用于报告日志信息:

1
2
3
4
5
6
7
8
9
package word

import "testing"

funcNonPalindrome(t *testing.T) {
if IsPalindrome("palindrome") {
t.Error(`IsPalindrome("palindrome") = true`)
}
}

还有类似 Printft.Errorf. t.Fatalt.Fatalf 可用于停止当前测试函数.

go test -v 用于打印每个测试函数的名字和运行时间.

参数 -run 对应一个正则表达式, 只有测试函数名正确匹配的函数才会被 go test 测试命令运行:

1
go test -v -run="French|Canal"

剖析

开启标志以生成分析文件:

1
2
3
go test -cpuprofile=cpu.out
go test -blockprofile=block.out
go test -memprofile=mem.out

示例函数

如:

1
2
3
4
5
6
7
func ExampleIsPalindrome() {
fmt.Println(IsPalindrome( "A man, a plan, a canal: Panama" ))
fmt.Println(IsPalindrome( "palindrome" ))
// Output:
// true
// false
}

其没有函数参数和返回值.

可用于:

  • 演示函数用法, 作为文档
  • go test 运行

12 反射

用于在运行时更新变量和检查他们的值, 调用它们的方法和他们支持的内在操作.

12.2 reflect.Type 和 reflect.Value 两个类型

reflect.TypeOf 方法 和 reflect.Type 类型

函数 reflect.TypeOf 接受任意的 interface {} 类型, 并以 reflect.Type 形式返回其动态类型, 其总是返回具体的类型:

1
2
3
t := reflect.TypeOf( 3 ) // a reflect.Type
fmt.Println(t.String()) // "int"
fmt.Println(t) // "int"

fmt.Printf%T 参数在内部使用 reflect.TypeOf 来输出.

reflect.ValueOf 方法 和 reflect.Value 类型

函数 reflect.ValueOf 接受任意的 interface {} 类型, 并以 reflect.Value 形式返回其动态值, 也是具体类型.

1
2
3
4
v := reflect.ValueOf( 3 )// a reflect.Value
fmt.Println(v) // "3"
fmt.Printf( "%v\n" , v) // "3"
fmt.Println(v.String()) // NOTE: "<int Value>"

fmt.Printf%v 参数在内部使用 reflect.ValueOf 来输出.

reflect.Value.Interface 方法, 是 reflect.ValueOf 的逆操作, 返回一个 interface {} 类型, 装载与 reflect.Value 相同的具体值.

reflect.Value 类型常用的还有:

  • Type(), 获取对应的 reflect.Type 类型值
  • CanAddr(), 判断是否能取址
  • IsValid(), 判断其值是否有效 (空指针或未初始化为无效)
  • Pointer(), 返回其存储的指针
  • Len(), 获取其长度, 适用于数组, 切片, 映射, 字符串等类型, 其余返回 0
  • Index(i int), 用于获取切片, 数组或字符串等类型索引为 i 的元素的 reflect.Value
  • Kind(), 返回 reflect.Value 的底层类型的枚举值
    方法.

12.5 通过 reflect.Value 修改值

一个变量就是一个可寻址的内存空间, 里面存储了一个值, 并且存储的值可以通过内存地址来更新.

有一些 reflect.Values 是可取地址的, 而有一些不行.

1
2
3
4
5
x := 2 // value type variable?
a := reflect.ValueOf( 2 ) // 2 int no
b := reflect.ValueOf(x) // 2 int no
c := reflect.ValueOf(&x) // &x *int no
d := c.Elem() // 2 int yes (x)

所有通过 reflect.ValueOf(x) 返回的 reflect.Value 都是不可取地址的 (都只是值的拷贝).

可以用 reflect.ValueOf(&x).Elem() 获取任意变量 x 对应的可取地址的 Value.

可以用 reflect.ValueCanAddr 方法判断其是否可以被取地址:

1
fmt.Println(a.CanAddr()) // "false"

每当通过指针间接地获取的 reflect.Value 都是可取地址的, 即使开始的是一个不可取地址的 Value.

13 底层编程

使用 unsafe 包, 其由编译器实现, 提供一些访问语言内部特性的方法. 可以通过 import 导入.

13.1 unsafe.Sizeof, Alignof 和 Offsetof

这几个函数对理解原生的内存布局有帮助.

unsafe.Sizeof 函数

unsafe.Sizeof 函数返回操作数在内存中的字节大小, 参数可以是任意类型的表达式, 但其不会对表达式进行求值.

如:

1
2
3
import "unsafe"

fmt.Println(unsafe.Sizeof( float64 ( 0 ))) // "8"

注意, 其只会计算类型中固定部分的大小, 不会包含其中指针指向的内存大小, 示例:

1
2
3
4
5
type Person struct {
Name string
Age int
Ptr *int
}

如果使用 unsafe.Sizeof(Person{}) 来计算 Person 类型的大小, 其只会计算 Name, Age 字段以及 Ptr 指针本身的大小.

unsafe.Alignof 函数

其返回对应参数的类型需要对齐的倍数.

unsafe.Offsetof 函数

其参数必须是一个字段 x.f, 然后返回 f 字段相对于 x 起始地址的偏移量, 包括可能的空洞.

13.2 unsafe.Pointer

unsafe.Pointer 是特别定义的一种指针类型, 类似 C 语言中的 void* 类型的指针, 可以包含任意类型变量的地址.

大多数指针类型会写成 *T, 表示是 “一个指向 T 类型变量的指针”.

一个普通的 *T 类型指针可以被转化为 unsafe.Pointer 类型指针, 并且一个 unsafe.Pointer 类型指针也可以被转回普通的指针 (并不需要与原类型相同).

示例:

1
2
3
4
5
6
7
package math

func Float64bits(f float64 ) uint64 {
return *(* uint64 )(unsafe.Pointer(&f))
}

fmt.Printf( "%#016x\n" , Float64bits( 1.0 )) // "0x3ff0000000000000"

13.3 深度相等判断

reflect.DeepEqual 函数可以对两个值进行深度相等判断. 其支持任意的数据类型, 会递归对变量做比较操作.

reflect.DeepEqual 函数的缺陷在于, 其会将一个 nil 值的 map 和非 nil 值但是空的 map 视作不相等, 同样 nil 值的 slice 和非 nil 但是空的 slice 也视作不相等.

(可查看书中的示例函数)

13.4 通过 cgo 调用 C 代码


Go语言圣经-Notes
http://example.com/2023/10/06/Go语言圣经-Notes/
作者
Jie
发布于
2023年10月6日
许可协议