Go_Language

[TOC]

Go 构建

环境变量 GOPATH 的值是一个目录的路径,也可以包含多个目录路径,每个目录都代表 Go 语言的一个工作区。这些工作区用于放置 Go 语言的源码文件、归档文件、可执行文件。

Go 语言源码的组织方式

Go 语言的源码是以代码包为基本组织单位的。在文件系统中这些代码包其实是与目录一一对应的。

每个代码包都有一个导入路径,导入路径是其他代码在使用该包中的程序实体时需要引入的路径。如下所示:

1
2

import "github.com/labstack/echo"

在工作区一个代码包的导入路径实际上就是从src子目录,到该包的实际存储位置的相对路径。

所以,Go 语言源码的组织方式就是以缓解变量 GOPATH、工作区、src目录和代码包为主线。

源码安装后的结果

  • 源码文件放在工作区的src 子目录下。

  • 归档文件放在该工作区的pkg子目录下,某个代码包产生的源码文件与归档文件同名。 导入路径为github.com/labstack/echo代码包的归档文件路径为pkg/linux_amd64/github.com/labstack/echo.a

  • 可执行文件放在该工作区的bin子目录下。

理解构建和安装的过程

go build

go build是构建命令,会执行编译、打包等操作,并且这些操作的结果会放在某个临时目录中。

  • 如果构建的是库源码文件,那么操作的结果文件只会存放于临时目录中,这里的构建的主要意义在于检查和验证。

  • 如果构建的是命令源码文件,那么操作的结果会被搬运到那个源码文件所在的目录中。

go build 常用选项

go build 默认不会编译目标代码包所依赖的哪些代码包。当然如果所依赖的代码包的归档文件不存在或者源码文件有了变化还是会被编译的。

-a 强制编译所依赖的代码包,包括所依赖的标准库中的代码包。加入-i还可以安装他们的归档文件。

-x 可以看到 go build命令具体都执行了哪些操作。

-n 只查看执行了哪些操作,而不执行它们。

-v 可以看到go build命令编译的代码包的名称,可配合-a标记搭配使用。

go install

go install 执行安装操作之前会先执行构建,然后还会进行连接操作,并且把结果文件搬运到指定目录。

  • 如果安装的是库源码文件,那么结果文件会被搬运到它所在工作区的pkg目录下的某个子目录中。

  • 如果安装的是命令源码文件,那么结果文件会被搬运到它所在工作区的bin目录中,或者环境变量GOBIN执行的目录。

构建和安装操作不同之处在于结果文件被搬运的位置。

go get

命令 go get会自动从一些主流公用代码仓库下载目标代码包,并把它们安装到GOPATH包含的第一工作区的相应目录中。

如果存在GOBIN,那么仅包含命令源码文件的代码包会被安装到GOBIN指向的目录。

常用标记

  • -u 强制下载安装代码包

  • -d 只下载代码包,不安装

  • -fix 下载代码包后,先运行一个根据当前 go 版本修正代码的工具,然后再安装代码包

  • -t 同时下载测试所需的代码包

  • -insecure 允许通过非安全的网络协议下载和安装代码包,例如 HTTP

自定义代码包导入路径

原本代码包的导入路径为:github.com/golang/sync/semaphore,如果在源码文件的包声明语句的右边加入导入注释

1
2

package semaphore // import "golang.org/x/sync/semaphore"

即可使用golang.org/x/sync/semaphore作为包的导入路径。

1
2

go get golang.org/x/sync/semaphore

程序实体是变量、常量、函数、结构体和接口的统称。

如何把命令源码文件中的代码拆分到其他源码文件中

在命令源码文件同目录下新建源码文件,包名与命令源码文件保持一致。然后命令源码文件可直接调用新建源码文件中的函数(函数名小写)。

  • 同目录下的源码文件的代码包声明语句要一致。也就是说,他们属于同一个代码包。

  • 如何目录中有命令源码文件,那么其他种类的源码文件也应该声明属于 main 包。

  • 源码文件声明的代码包名可以与其所在的目录的名称不同。 构建时生产的结果文件的主名称与其父目录的名称一致。但为了混乱一般代码包名和目录名保持一致。

怎么把命令源码文件中的代码拆分到其他代码包中

源码文件所在的目录相对于 src 目录的相对路径就是它的代码包导入路径,而实际使用其程序实体时给定的限定符要与它声明所属的代码包名称对应。

  • 包名与源码文件所在的目录名保持一致。

  • 将相对于 src 目录的相对路径作为导入路径

  • 将需要对外使用的方法首字母大写。

import导入路径最后一级相同

  • 如果声明的包名相同,则引发冲突,

  • 如果包名不同则不会冲突

包名冲突的解决办法

  • 设置别名,import (b "fmt")
  • 导入点操作 , import (. "fmt") ,调用时不需要包名,可直接调用函数
  • 只引入包而没有在代码中实际调用,import (_ "fmt")

命令源码文件

源码文件由命令源码文件、库源码文件、测试源码文件组成。

命令源码文件可以通过构建或安装生成与其对应的可执行文件,后者一般会与该命令源码文件的直接父级目录同名。

例如源码文件属于main包,且包含一个无参数声明且无结果声明的main函数,那么就是命令源码文件。

对于一个独立的程序来说,命令源码文件有且只能有一个。

私有的命令参数容器

Go 原理

Go 是按值调用的,调用函数接收到的是实参的一个副本,并不是实参的引用。

golang.org/x/...下的仓库都由 Go 团队负责设计和维护。这些包不属于标准库。

前缀Must开头的函数表示不应该接受的不正确的值,例如template.Must

Go 语言中封装的单元是包而不是类型。

要封装一个对象,必须使用结构体。

一些建议

不要先定义一系列接口,再定义具体的实现,而且这些接口只有一个单独的实现。这种接口抽象是不必要的。

可以使用导出机制来限制一个类型的方法、结构体的字段的可见性。

仅有在有多个具体类型需要按照统一的方式处理时才需要接口,或者接口和类型实现处于依赖的原因不能放到同一个包里面,这是接口是解耦两个包的最好方式。

基本概念

  • 每个 Go 程序都是由包构成,通过import导入包,按照约定包名与导入路径的最后一个元素一致,例如import "fmt"import "math/rand"
  • 多个导入语句可以合并
1
2
3
4
5
6
7
8

import (

"fmt"

"math"

)
  • 导出的函数首字母必须大写,类似于 Java 的public方法。

  • 函数声明

1
2
3
4

func add (x int, y int) int {
return x + y
}
  • 多个连续相同类型的参数,类型名可以合并,例如func add( x, y int) int ;

  • 返回值可以返回任意数量的返回值

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

func swap ( x, y string) (string, string) {

return y, x

}

func main() {

a, b := swap("hello", "world!")

}
  • Go 的返回值可被命名,会被视为定义在函数顶部的变量。 采用没有参数的return语句返回已命名的返回值。
1
2
3
4
5
6
7
8
9
10

func split(sum int) (x, y int) {

x = sum * 4 / 9

y = sum - x

return

}
  • var 用于声明变量,可声明变量列表,类型在最后。 可以出现在包或函数级别。

  • 变量声明可以包含初始值,每个变量对应一个。初始化声明时可省略类型,从初始值中获得类型。

1
2
3
4

var i, j int = 1, 2

var c, python, java = true, false, "no!"
  • 短声明用于声明局部变量可在类型明确的地方代替var声明。
1
2
3
4

var i, j int =1, 2

k := 3
  • 表达式 new(T) 创建一个未命名的 T类型变量,初始化为 T 类型的零值,并返回其地址,地址类型 *T,每次调用 new 都会返回不同的地址,但是两个变量的类型不携带任何信息且是零值,例如 struct{} int[0] ,在当前实现中有相同地址。

  • 函数外的每个语句必须以关键字开始,例如varfunc

  • 基本类型

    • bool

    • string

    • int、int8、int16、int32、int64。int 在32位系统为32,在64位为64位

    • uint、uint8、unit16、unit32、uint64、uinptr。uint、uintptr 同 int

    • byte,uint8的别名

    • rune,int32的别名,表示一个 Unicode

    • float32、float64

    • complex64、complex128

    • 除非有特殊理由,一般整数值应使用int类型

  • 没有明确初始值的变量声明会被赋予它们的零值,零值有:

    • 数值类型为0

    • 布尔类型为 false

    • 字符串为""

  • Go 在不同类型的项之间赋值需要显示转换。T(v)表示将值v转换为类型T。例如var f :=float64(64)

  • 常量不能用:=语法声明。const Pi = 3.14

变量的声明周期

生命周期是指程序执行过程中变量存在的时间段。包级别的变量的生命周期是全局的。局部变量的生命周期一直存在直到不可访问。

包初始化

包的初始化从初始化包级别的变量开始,按照声明顺序初始化。

在每个文件里,当程序启动的时候,init 函数按照它们声明的顺序自动执行。

包的初始化,按照程序中导入的顺序来进行,依赖顺序优先,main 包最后初始化。

作用域

声明的作用域是声明在程序文本中出现的区域,它是编译时属性。变量的生命周期是变量在程序执行期间能被程序的其他部分所引用的起止时间,它是运行时属性。

在包级别,声明的顺序和它们的作用域没有关系,所以一个声明可以引用它自己活着跟在它后面的其他声明。

常量

常量生成器iota,在常量声明中,iota 从 0 开始取值,逐项加 1.

1
2
3
4
5
6
7
8
9
10
type Weekday int 
const (
Sunday Weekday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
)

从属类型待定的常量共有 6 种,分别是无类型布尔、无类型整数、无类型文字符号、无类型浮点、无类型复数、无类型字符串。

  • Go 只有一种循环结构 for循环,花括号是必须的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

for i:=0; i<10;i++ {

// do something

}

sum := 1

for sum < 100 {

//do something

}

for { // 无限循环

}
  • if语句的表达式也不需要括号,但是花括号是必须的。而且可以在比较表达式前加一个简单的语句
1
2
if v:=100; v>10 {
}
  • switch语句类似ifcase不需要break,它只运行指定的case,且无需为常量。
1
2
3
4
5

switch os:= runtime.GOOS; os {
case "darwin":
case "linux" :
default :
  • switchcase语句按照从上到下顺次执行。直到匹配成功为止。
  • 没有条件的switchswitch true一样
  • defer语句会将函数推迟到外层函数返回之后才被调用。 推迟调用的函数其参数会立即求值。
  • 推迟的函数调用会被压入一个栈中。当外层函数返回同时,被推迟的函数会按照先进后出的顺序调用。

类型声明

type 声明定义一个新的命名类型,它和某个已有类型使用相同的底层类型。type name underlying-type

对于每个类型 T,都有一个对应的类型转换操作 T(x) 将值转换为类型 T,如果两个类型具有相同的底层类型或者二者都指向相同底层类型变量的未命名指针类型,则二者是可以相互转换的。

命名类型的底层类型决定了它的结构和表达方式,以及它支持的内部操作集合,这些操作与直接使用底层类型的情况相同。

基本类型

字符串

格式化

字符 含义
%t 输出一个布尔值
%T 输出一个值得类型,包括函数签名
%x 将一个数组或者 slice 里面的字节按照十六进制的方式输出
%#v 以类似 Go 语法的方式输出对象

##高级类型

指针

  • *T表示指向T类型值的指针,其零值为nil

  • &会生成一个指向其操作数的指针。 Go 没有指针运算

1
2
3
4
5
6
7
8

var p *int

i := 42

p = &i

*p = 21

结构体

结构体就是一个字段的集合。

1
2
3
4
5
6
7
8
9
10
type Vertex struct {
X int
Y int
}
v := Vertex{1, 2}
v.X = 4
p := &v // p.Y 其实是(*p).Y的简写。
c := v
p.Y = 1
c.X = 1
  • 命名结构体类型 S 不可以定义一个相同结构体类型 S 的成员变量,但是可以定义一个S的指针类型 *S

  • 成员变量的顺序对结构体同一性很重要,顺序不一样视为不同的结构体类型。

  • 结构体的零值由结构体成员的零值组成。

  • 没有任何成员变量的结构体称为空结构体,写作struct{} 。它没有长度,不携带任何信息。例如当做 map 的值类型,来表示只有键是有用的。

结构体字面量

1
2
3
4
type Point struct{X, Y int}

p := Point{1,2} //按照正确的顺序为每个成员赋值
p1:= Point{X:1} // 指定成员变量的名称和值来初始化,未指定的成员为零值

​ 通常结构体使用指针的方式使用。

1
2
3
4
pp := &Point{1, 2} 
//等价于
pp := new(Point)
*pp = Point{1, 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
type Point struct {
X, Y int
}

type Circle struct {
Point
Radius int
}

type Wheel struct {
Circle
Spokes int
}
var w Whell
w.X = 8 // 等价于 w.Circle.Point.X = 8

w = Wheel{Circle{Point{8, 8}, 5}, 20} // 结构体字面量没有快捷方式来初始化结构体
w1 = Wheel {
Circle : Circle {
Point: Point{X:8, Y:8},
Radius: 5,
},
Spokes: 20,
}

​ 匿名成员的可导性由它们的类型决定的,即使这两个结构体不可导出(pointcircle),仍然可以使用快捷方式 w.X=8,但是不能在包外使用 w.circle.point.X = 8

数组

数组时具有固定长度且拥有零个或多个相同数据类型元素的序列。长度是数组类型的一部分,所以[3]int[4]int是不同的数组类型。数组类型必须是常量表达式。

1
2
3
4
var a [2]string // 元素为对应类型的零值
primes := [6]int{2,3,4,5,6,7}
var c = [3]int = [3]int{1,2,3} //数组字面量初始化数组
b := [...]string{"P", "T"} // 自动统计数组长度

也可以给出对应的索引和索引值,没有指定索引位置的元素赋予零值

1
2
3
4
5
6
7
8
9
type Currency int 
const (
USD Currency = iota
RMB
)
symbol :=[...]string{USD: "$", RMB:"¥"}
fmt.Println(RMB, symbol[RMB]) // "1 ¥"

r := [...]int{99:-1} //其余元素都是 0

如果数组的元素是可以比较的,那么数组也是可以用==比较的,比较的结果是两边元素的值是否完全相同,包括顺序。

slice

slice 表示一个拥有相同类型元素的可变长度的序列。slice 通常写成 []T,看起来像是没有长度的数组类型。

slice 有三个属性:指针、长度和容量。指针指向数组第一个可以从 slice 中访问的元素,长度值 slice 中元素的个数,容量指从 slice 起始元素到底层数组最后一个元素间元素的个数。lencap可以分别返回 slice 的长度和容量。

1
2
3
4
months := [...]string{1:"January", /*...*/,12: "December"}
summer := months[6:9] // 不包含 9
fmt.Println(summer[:20]) // 宕机,超过了容量
fmt.Println(summer[:5]) // 扩展了 slice "[June July August September October]"
  • slice不会创建新的数组,而是原数组的引用。更改slice会导致原数组也更改
  • 注意,slice a[0:len(a)]a[:len(a)]a[0:]a[:]是等价的。

slice 的比较

slice 无法使用==做比较,可以使用bytes.Equal来比较两个字节 slice([]byte)。

slice 唯一允许的比较操作是 nil ,其值为 nil的 slice 没有对应的底层数组、长度和容量都是零。反之不成立。

1
2
3
4
var s []int    // len(s) == 0, s == nil
s = nil // len(s) == 0, s == nil
s = []int(nil) // len(s) == 0, s == nil
s = []int{} // len(s) == 0, s != nil

所以推荐使用len(s) ==0来检查 slice 是否为空。 另外除非特殊标注,无论值是否为nil,Go 函数都应该以相同的方式对待所有长度为零的 slice。

func make([]T, len, cap) []T函数会分配一个元素为零值的数组并返回一个引用了它的切片。

1
2
make([]int, 5) // len = 5
make([]int, 5, 6) // len = 5 cap = 6

append 函数

append函数用来将元素追加到 slice 后面。

1
2
3
4
var s []int
s = append(s, 10)
s = append(s, 11, 13, 15)
s = append(s, b...) // b 为另一个切片

append函数的原理是,如果容量足够则创建一个新的 slice,底层数组不变;如果容量不够,创建一个新的底层数组,并copy 元素到新数组。

此外虽然底层数组的元素是间接引用的,是 slice 的指针、长度、和容量不是,因此只要是有可能改变 slice的长度、容量或是使得 slice 指向不同底层数组的操作,都需要更新 slice。

可能的陷阱

因为切片引用了原始的数组的空间,导致 GC 不能释放数组空间:只用到少数元素却导致整个数组的内容一值保存在内存里。解决方法就是将感兴趣的数据复制到一个新的切片中。

字符串子串操作和堆字节 slice ([]byte)做 slice 操作的区别:如果 x 是字符串,那么x[m:n]返回的是一个字符串,反之返回的是自己 slice

map

map 是无序键值对的集合,其中k 必须是可以通过 ==来进行比较的数据类型。

1
2
3
4
5
6
args := make(map[string]int) // 创建一个从 string 到 int 的 map
args := map[string]int{
"alice": 31,
"charlie": 34
}
delete(args, "alice") // 移除元素

新的空 map 的另外一种表达式是map[string]int{}

无法获取 map 元素的地址,一个原因是 map 的增长导致已有元素被重新散列到新的存储位置,这样导致获取的地址无效。

map 的零值是nil,大多数操作可以安全的在 map 的零值上执行,包括查找元素、删除元素、len(map)、执行range循环,这和空 map 行为一致,但是向零值 map 设置元素回导致错误。

键不在 map 中,将得到 map 值类型的零值,但是如何辨别出一个不存在的元素或者恰好是 0 的元素,可以这样做

1
2
3
if age, ok := ages["bob"]; !ok {
/* ....*/
}

JSON

json.Marshal可将 Go 对象序列化为 json 字符串,json.MarshalIdent可以输出整齐格式化过的结果,这个函数有两个参数,第一个参数定义每行输出的前缀字符串,另外一个定义缩进的字符串。

只有可导出的成员可以转换为JSON 字段,另外Year对应的转换为releasedomitempty的含义表示Color是零值或者为空时,不输出这个成员到 JSON 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type Movie struct {
Title string
Year int `json:"released"`
Color bool `json:"color,omitempty"`
Actors []string
}
var movies = []Movie{
{Title: "Casablanca", Year: 1942, Color: false,
Actors: []string{"Humphrey Bogart", "Ingrid Bergman"}},
{Title: "Cool Hand Luke", Year: 1967, Color: true,
Actors: []string{"Paul Newman"}},
{Title: "Bullitt", Year: 1968, Color: true,
Actors: []string{"Steve McQueen", "Jacqueline Bisset"}},
// ...
}

data, err := json.Marshal(movies)
if err != nil {
log.Fatalf("JSON marshaling failed: %s", err)
}
fmt.Printf("%s\n", data)

data, err := json.MarshalIndent(movies, "", " ")

unmarshal 将 JSON 字符串反序列化成 Go 对象。

1
2
3
4
5
6
var titles []struct{ Title string }

if err := json.Unmarshal(data, &titles); err != nil{
log.Fatalf("JSON unmarshaling failed: %s", err)
}
fmt.Println(titles)

json.newDecoder(io.Reader).Decoder 流式解码器,依次从字节流里面解码出多个 JSON 实体。同样,也有json.newEncoder(io.Writer).Encoder流式编码器。

文本和 HTML 模板

​ 模板可以将格式和代码分离,模板是一个字符串或者文件,包含多个用{{}} 包围起来的操作。操作可以输出值、选择结构体成员、调用函数和方法、描述控制逻辑、实例化其他模板等。这些都通过text/templatehtml/template包里面的方法实现。

1
2
3
4
5
6
7
const templ = `{{.TotalCount}} issues:
{{range .Items}}----------------------------------------
Number: {{.Number}}
User: {{.User.Login}}
Title: {{.Title | printf "%.64s"}}
Age: {{.CreatedAt | daysAgo}} days
{{end}}`

.TotalCount中的 .表示模板里面的参数,.Number中的.表示Items里面连续的元素。{{range .Items}}{{end}}表示创建一个循环。 |会将前一个操作的结构当做下一个操作的输入。printf是内置函数,fmt.Sprintf的同义词。

​ 通过模板输出结果需要两步

  1. 解析模板并转换为内部的表示方法

    1. 在指定的输入上面执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
report, err := template.New("report").
Funcs(template.FuncMap{"daysAgo": daysAgo}).
Parse(templ)
if err != nil {
log.Fatal(err)
}
// 或者使用 template.Must
var report = template.Must(template.New("issuelist").
Funcs(template.FuncMap{"daysAgo": daysAgo}).
Parse(templ))

if err := report.Execute(os.Stdout, result); err != nil {
log.Fatal(err)
}

template.Must提供了一种便捷的处理错误方式,接受模板和 err,如果err!=nil 则宕机,然后返回这个模板。

html/templatetext/template 有同样的 API,但会对字符串进行转义,防止注入攻击。

我们可以使用template.CSStemplate.JStemplate.URLtemplate.HTML来处理受信任的 CSS、JavaScript、URL、HTML 文本。

函数值

  • 函数也是值,也可以像其他值一样传递,可以用作函数的参数以及返回值。
1
2
3
4
5
6
7
8
9
10
11
12

func compute(fn func(float64, float64) float64) float64 {
return fn(3, 4)
}
func main() {
hypot := func(x, y float64) float64 {
return math.Sqrt(x*x + y*y)
}
fmt.Println(hypot(5, 12))
fmt.Println(compute(hypot))
fmt.Println(compute(math.Pow))
}
  • 函数可以是一个闭包。引用其函数体之外的变量。该函数可以访问并赋予其引用的变量的值,换句话说,该函数被 “绑定” 在了这些变量上。例如,函数 adder 返回一个闭包。每个闭包都被绑定在其各自的 sum 变量上。

  • 注意闭包没有方法名

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

func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
func main() {
pos, neg := adder(), adder()
for i := 0; i < 10; i++ {
fmt.Println(
pos(i),
neg(-2*i),
)
}
}

##函数

函数声明

每个函数声明包含一个名字、形参列表、可选的返回列表以及函数体

1
2
3
func name(parameter-list)(result-list){
body
}

当只有一个返回值或者没有返回值时,返回列表的括号可以省略。

多个形参或者返回值的类型相同,可以只需写一次。

1
func f(i, j, k int, s, t string){}

函数的类型称为函数签名,拥有相同形参列表和返回列表的函数的类型或签名是相同的。

Go 没有默认参数值的概念,也不能指定实参名。

函数形参和命名返回值都属于函数最外层的作用域的局部变量。

实参是按值传递的,所以函数接收到的是每个实参的副本,修改形参变量不会影响到实参,包括对象、数组等。如果提供的实参包含引用类型,比如指针、slice、map、函数或者通道,那么函数就有可能间接的修改实参变量。

有些函数的声明没有函数体,那说明这个函数使用了 Go 以为的语言实现。

函数如果有命名的返回值,则可以省略return语句的操作数,这称为裸返回。但不推荐这么做,不便于理解。

错误

当函数调用发生错误时返回一个附加的结果作为错误值,习惯上将错误作为最后一个结果返回。如果错误只有一种情况,通常设置为布尔类型。更多的时候,返回 error类型的结果。

当函数返回一个非空错误时,其他结果都是未定义且应该忽略的。

处理错误策略

  1. 将错误传递下去,使用fmt.Errorf 格式化一条错误消息并且返回一个新的错误值。设计一个错误消息应当谨慎,确保每一条错误消息都是有意义的,包含充足的相关信息,并且保持一致。
  2. 重试若干次数,超过之后再报错退出。
  3. 输出错误,然后调用os.Exit(1)优雅的停止程序。
  4. 记录错误信息,然后继续运行程序
  5. 直接忽略

函数变量

函数可以赋给变量、传递或者从其他函数中返回。函数签名不同的变量不可赋值。函数可以和 nil比较,但是本身不可比较。

匿名函数

命名函数只能在包级别的作用域进行声明,函数字面量可在任何表达式内指定函数变量。

通过函数字面量定义的函数能够取到整个词法环境,里层的函数可以使用外层函数的变量。

1
2
3
4
5
6
7
func squares() func() int {
var x int
return func() int {
x++
return x*x
}
}

陷进:捕获迭代变量

1
2
3
4
5
6
7
8
9
10
11
var rmdirs []func()
for _, d := range tempDirs() {
dir := d // ①
os.MkdirAll(dir, 0755)
rmdirs = append(rmdirs, func(){
os.RemoveAll(dir)
})
}
for _, rmdir := range rmdirs {
rmdir() //清理
}

d变量的值在不断迭代中更新,因此当调用清理函数的时候,假如没有①这一步的话,所有的os.RemoveAll调用最终都试图删除同一目录,最后一次迭代时的目录。

变长函数

在参数列表最后的类型名称之前使用省略号,表示声明一个变长函数,调用时可以传递任意数目的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
func sum(vals ...int) int {
total := 0
for _, val := range vals {
total += val
}
return total
}
fmt.Println(sum())
fmt.Println(sum(3))
fmt.Println(sum(2,3,4))

values := []int{1,2,3,4,5}
fmt.Println(sum(values...))

interface{} 类型意味着函数可以接受任何值。

延迟函数调用

defer 语句修饰的函数调用,会推迟到包含defer语句的函数执行完毕后才执行,不论函数是正常执行完毕,还是异常退出。defer 语句没有限制使用次数,执行的时候以调用defer语句顺序的倒序进行。

defer一般用于释放资源,正确使用defer语句的地方是成功获得资源之后。

1
2
3
4
5
6
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()
return ReadAll(f)

defer也可用于在函数入口、出口处设置调试行为

1
2
3
4
5
6
7
8
9
10
11
func bigSlowOperation() {
defer trace("bigSlowOperation")() // don't forget the extra parentheses

time.Sleep(10 * time.Second)
}

func trace(msg string) func() {
start := time.Now()
log.Printf("enter %s", msg)
return func() { log.Printf("exit %s (%s)", msg, time.Since(start)) }
}

defer执行的函数在return语句之后,因此可以打印返回的结果,甚至修改返回结果的值。

panic 和 recover

Go 语言在运行时检测到一些错误会发生宕机,例如数组越界。一个典型的宕机发生时,正常的程序执行会终止,goroutine 中的所有延迟函数会执行,然后程序会异常退出并留下一条日志消息。

panic 是内置的宕机函数,可以接受任值作为参数。可以使用该函数来处理不可能发生的错误。

runtime包提供了转储栈的方式,使程序员可以诊断错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
defer printStack()
f(3)
}
func printStack() {
var buf [4096]byte
n := runtime.Stack(buf[:], false)
os.Stdout.Write(buf[:n])
}
func f(x int) {
fmt.Printf("f(%d)\n", x+0/x) // panics if x == 0
defer fmt.Printf("defer %d\n", x)
f(x - 1)
}

退出程序通常是正确处理宕机的方式,但也有例外,例如 web 程序。

如果内置的recover 函数在延迟函数的内部调用,并且这个包含defer语句的函数发生宕机,recover会终止当前宕机状态,并且返回宕机的值。函数不会继续运行而是正常返回。

从同一个包内发生的宕机进行恢复有助于简化处理复杂和未知的错误,但一般的原则是,不应该尝试恢复从另一个包内发生的宕机,尤其是第三方API 的宕机,因为你不知道这样做是否安全。

有些情况下是没有恢复动作的,例如内存耗尽使得 Go 运行时发生严重错误。

方法

在 Go 中方法和函数是有区别的。方法是指和某个类型绑定的函数。因此方法的声明和函数类型,只是前面多了一个参数,要绑定的类型。方法和字段使用同一个命名空间,不可重名。

1
2
3
4
5
type Point struct{ X, Y float64}

func (p Point) Distance(q Point) float64{
return math.Hypot(q.X - p.X, q.Y-P.Y)
}

附加的参数 p成为方法的接受者。表示主调方法就像向对象发送消息。

Go 可以将方法绑定到任何类型上,包括Go 本身内置的简单类型。

指针接受者的方法

由于主调函数会复制每一个实参变量,如果函数需要更新一个变量,或者实参太大希望避免复制整个实参,因此我们必须使用指针来传递变量的地址。这也适用于更新接受者。

1
2
3
4
func (p *Point) ScaleBy(factor float64){
p.X *= factor
P.Y *= factor
}

习惯上,如果一个类型的任何一个方法使用指针接受者,那么该类型的所有方法都应该使用指针接受者,即时有些方法不一定需要。

合法的方法调用

  1. 实参接收者和形参接受者是同一类型,都是T类型或者*T类型。
  2. 实参接收者是T类型,而形参接受者是*T类型,编译器或隐式的获取变量的地址
  3. 实参接收者是*T类型,而形参接受者是T类型,编译器或隐式地解引用接受者,获取实际取值。获取不到是不允许的。

nil 是一个合法的接受者

结构体内嵌

内嵌使得一个结构体包含另外一个结构体的所有字段,这条规则也同样适用于方法。

结构体内嵌看来像是面向对象语言的继承。但是实质上,内嵌更像是编译器生成额外的包装方法来调用内嵌结构体声明的方法。

1
2
3
4
5
6
7
8
9
type ColoredPoint struct {
Point
Color color.RGBA
}

// 实质上相当于以下代码
func (p ColoredPoint)Distance(q Point) float64 {
return p.Point.Distance(q)
}

编译器处理选择方法时,首先查找直接声明的方法、之后从内嵌字段的方法中查找、再之后从内嵌的内嵌字段的方法查找,以此类推。当同一个查找级别中有同名方法时,会报错。

方法变量与方法表达式

1
2
3
4
5
6
7
p := Point{1, 2}
q := Point{4, 6}
distanceFromP := p.Distance // 方法变量
fmt.Println(distanceFromP(q))

distance := Point.Distance // 方法表达式
fmt.Println(distance(p, q))

接口

接口类型是对其他类型行为的概括和抽象。通过接口可以写出更加灵活和通用的函数,这些函数不必绑定在特定的类型实现上。

Golang 的接口是隐式实现的,对于一个具体类型,无须声明它实现了哪些接口,只要提供接口所必须的方法即可。

1
2
3
type Writer interface {
Write(p []byte)(n int, err error)
}

与结构体类似,接口也可以通过组合已有接口得到新的接口。

1
2
3
4
type ReadWriter interface {
Reader
Writer
}

一个具体类型通过实现一个接口的所有方法来实现该接口。仅当一个表达式实现了一个接口时,这个表达式才可以赋给该接口。

interface{}空接口类型不包含任何实现,所以任何值都可以赋给空接口类型。

断言某个类型实现了每个接口

1
var _ io.Writer = (*bytes.Buffer)(nil)

接口值

一个接口类型的值分为两部分,一个具体类型和该类型的值,称为接口的动态类型和动态值。

类型描述符提供每个类型的具体信息,包含它的名称和方法。

1
2
var w io.Writer //动态类型和值都设为 nil
w = os.Stdout

赋值把一个具体类型隐式转换为一个接口类型,与显示转换io.Writer(os.Stdout)等价。接口值的动态类型会设置为指针类型*os.File的类型描述符,它的动态值会设置为os.Stdout的副本,即一个指向代表进程的标准输出的os.File类型的指针。

接口值可以用==!=操作符来比较,如果两个接口值都是nil或者二者的动态类型完全一致且二者动态值相等,那么两个接口值相等。

如果两个接口值动态类型一致,但是动态值不可比较,会产生宕机。把接口作为 map 键或者switch 语句的操作数时,也存在类似风险。

1
2
var x interface{} = {}int{1,2,3}
fmt.Println(x == x) //宕机

可能存在的陷阱 : 含有空指针的非空接口

空的接口值与动态值为nil的接口值是不一样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const debug = true
func main(){
var buf *bytes.Buffer
if debug {
buf = new(bytes.Buffer)
}
f(buf)
if debug {
// 使用 buf
}
}
func f (out io.Writer) {
if out != nil {
out.Write([]byte("done!\n"))
}
}

debug =false 时,out 的动态值确实为空,但是动态类型是*bytes.Buffer ,这表示out是一个包含空指针的非空接口,所以out !=niltrue

正确的做法是将 buf 修改为 io.Writer

1
2
3
4
5
var buf io.Writer
if debug {
buf = new (bytes.Buffer)
}
f (buf)

类型断言

类型断言是一个作用在接口值上的操作,写法类似x.(T),其中x 是一个接口类型的表达式,而T是一个类型。类型断言会检查作为操作数的动态类型是否满足指定的断言类型。如果检查成功返回x的动态值。

类型断言就是从它的操作数中把具体类型的值提取出来的操作。检查失败会崩溃。

如果采用两个结果的赋值表达式,那么断言失败不会崩溃。而是返回一个布尔类型的返回值指示断言是否成功。

1
f, ok := w.(*os.File)

一个具体类型是否满足某个接口,仅仅由它拥有的方法来决定,而不是这个类型与一个接口类型之间的关系声明。这跟 Java 这种强类型不同。

类型分支

接口有两种不同的风格。第一个风格下,接口上的各种方法突出满足这个即可的具体类型之间的相似性,强调方法。第二种风格充分利用了接口值能够容纳各种具体类型的能力,把接口作为这些类型的联合来使用,通过类型断言用来运行时区分这些类型并分别处理。强调的是接口的具体类型,这种风格成为可识别联合。

1
2
3
4
5
6
switch x.(type){
case nil: // x==nil
case int, uint:
case bool :
default :
}

类型分支扩展形式,这里类型分支会隐式创建了一个词法块,每个分支也会隐式创建各自的词法块.

1
switch x:= x.(type)

错误

error实际上是接口类型,完整的error包只有如下代码。

1
2
3
4
5
6
7
8
9
type error interface {
Error() string
}

package errors

func New(text string) error { return &errorString{text}}
type errorString struct { text string}
func (e *errorString) Error() string { return e.text}

一般很少使用errors.New,更多使用fmt.Errorf

并发

Go 有两种并发编程风格,一种是 goroutine 和 channel,它们支持通信顺序进程(Communicating Sequential Process, CSP)CSP 是一种并发的模式,在不同执行体之间传递值,单变量本身局限于单一的执行体。

另外一种是共享内存的多线程的传统模型。

goroutine 和通道

在 Go 中每一个并发执行的活动称为 goroutine。可以假设 goroutine 类似于线程。

程序启动时,只有一个 goroutine 来调研main函数,称为主 goroutine。当 main函数返回时,所有的 goroutine 都暴力地直接终结。

除了main函数返回或者程序终结没有程序化的方式让一个 goroutine 来停止另外一个,但可以和 goroutine 通信要求它自己停止。

go 语句使得函数在一个新创建的 goroutine 中调用,实参会在当前 goroutine 中计算完毕。

通道

通道是用来连接 goroutine,可以让一个 goroutine 发送特定值到另一个 goroutine 的通信机制。每个通道是一个具体类型的导管,叫做通道的元素类型。

channel 有三种操作,send、receive、close,通道使用make 创建数据结构的引用,当复制或者作为参数传递的时候,复制的是一个引用。通道的零值是nil

1
2
3
4
5
6
ch := make(chan int) // int 类型通道,无缓冲
ch = make(chan int, 4) // 创建容量 4 的缓存通道
ch <- x // 发送语句
x = <- ch // 接受
<- ch // 接受,丢弃结果
close(ch) //关闭

close 操作设置一个标志位来表示当前已经发送完毕,关闭后的发送操作会导致宕机。可通过x, ok := <- ch方式判断通道是否关闭。也可以使用range语法糖,当通道关闭后循环自动结束。

通道不是必须关闭的,垃圾回收器会根据是否可以访问来决定是否回收。只有在通知接收方所有数据都发送完的时候才有需要关闭通道。关闭一个已经关闭的通道会宕机。

1
2
3
for x := range ch {

}

无缓冲通道的发送和接受将会阻塞,直到收到接受或者发送请求。无缓冲通道也称为同步通道。

Go 类型系统提供了单向通道类型,chan <- int 是一个只能发送的通道,<- chan int 是一个只能接受的通道。

缓存通道有一个元素队列,发送操作在队列尾部插入一个元素,接受操作从队列头部移除一个元素,如果通道满了,发送和接受操作将会阻塞。

cap(ch) 获取通道的容量,len(ch)获取通道元素数量。

select多路复用

select 类似于switch,每个 case 必须是一个通信操作,要么发送要么接受,select 随机执行一个可运行的case,假如没有将阻塞,直到有可运行的 case。

使用共享变量实现并发

数据竞态发生在两个 goroutine 并发读写同一个变量并且至少其中一个是写入时。有三种方法避免数据竞态:

  1. 不要修改变量
  2. 避免从多个 goroutine 访问同一个变量。Go 箴言的含义,不要通过共享内存来通信,应该通过通信来共享内存。
  3. 允许多个 goroutine 访问同一变量,但是同一时间只有一个 goroutine 可以访问。

互斥锁 sync.Mutex

Go 语言的互斥量是不可再入的。

1
2
3
4
5
6
var mu = sync.Mutex
func Balance() int {
mu.Lock()
defer mu.Unlock()
return balance
}

读写互斥锁 sync.RWMutex

使用RLockRUnlock 方法来分别获取和释放一个读锁,LockUnlock 来分别获取和释放一个写锁。

RWMutex 适用于大部分 goroutine 都在获取读锁并且锁竞争比较激烈时,才会有优势。在竞争不激烈时比普通的互斥锁慢。

内存同步

对于Balance只包含单个操作的方法也需要加锁的原因有两个,一是防止其插到其他操作中间,二是涉及到内存同步问题。

1
2
3
4
5
func Balance() int {
mu.Lock()
defer mu.Unlock()
return balance
}

计算机中每个处理器都会有内存的本地缓存,对内存的读写都是缓存在处理器中,只有必要时才刷回内存。通道通信、互斥锁操作这种同步语句都会导致处理器把写操作刷回内存并提交。

延迟初始化 sync.Once

sync.Once提供了针对一次性初始化问题的特化解决方案,向Do方法传入初始化函数作为它的参数。

1
2
3
4
5
6
var loadIconsOnce sync.Once
var icons map[string]image.Image
func Icon(name string) image.Image {
loadIconsOnce.Do(loadIcons)
return icons[name]
}

竞态检测器

-race 命令行参数加到go buildgo rungo test 命令中,即可使用竞态检测器。它会让编译器为你的应用构建一个修改后的版本,这个版本会额外记录在执行时对共享变量的所有访问、读写这些变量的 goroutine 标识、记录所有同步事件,包括 go 语句、通道操作、(*sync.Mutex).Lock 调用、(*sync.WaitGroup).Wait 调用等。

竞态检测器只会报告所有实际运行了的数据竞态,请确保测试包含了所有并发场景。

goroutine 与线程

每个 OS 线程都有一个固定大小的栈内存,用于保存在其他函数调用期间哪些正在执行或临时暂停的函数中的局部变量。goroutine 的栈不是固定大小,典型情况下为 2KB,最高可达 1GB。

OS 线程由 OS 内存来调度,每个几毫秒,一个硬件时钟中断发到 CPU,CPU 调用一个叫调度器的内核函数,这个函数暂停当前正在运行的线程,把它的寄存器信息保存到内存,查询线程列表并决定接下来运行哪一个线程,再从内存恢复线程的注册表信息,最后执行选中的线程。所以 OS 切换线程需要一个完整的上下文切换:保存一个线程的状态到内存,再恢复另外一个线程状态,最后更新调度器的数据结构。

Go 运行时包含一个自己的调度器,这个调度器使用一个称为m:n调度的技术(可以复用/调度 m 个 goroutine 到 n 个 OS 线程)。Go 调度器不时有硬件时钟定期触发,而是特定的 Go 语言结构触发,当一个 goroutine 调用time.Sleep或被通道阻塞或对互斥量操作时,调度器就会将这个 goroutine 设为休眠模式,并运行其他 goroutine 直到前一个可重新唤醒为止。不需要切换到内核语境,调用一个goroutine 比调度一个线程成本低很多。

Go 调度器使用GOMAXPROCS 参数(m:n 调度中的 n)来确定需要多少个 OS线程来同时执行 Go 代码,默认是 CPU 数量。正在休眠或者通道阻塞的 goroutine 不需要占用线程。阻塞在 I/O 和其他系统调用中或调用非 Go 语言写的函数goroutine 需要一个独立的 OS 线程,这个线程不计算在 GOMAXPROCS内。

Go 没有线程标识,其他支持多线程的编程语言中,当前线程都有一个独特的标识,这个特性可以构建一个线程的局部存储。但 Go 的 goroutine 没有,不支持该特性。

包和工具

包简介

包控制名称是否导出使其对包外可见来提供封装能力。

除了标准库中的包,其他包的导入路径应该以互联网域名作为路径开始。

每一个 Go 源文件的开头都要进行包声明package main,主要目的是当该包被其他包引入的时候作为包名,通常是导入路径的最后一段。但有三个例外:

  1. 使用main名称,表示必须调用连接器生成可执行文件
  2. _test.go结尾,用于测试
  3. 一些依赖工具会在包的导入路径的尾部追加版本后缀

导入两个名称一样的包时,可以指定一个代替名称避免冲突,仅影响当前文件。

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

导入的包会建立依赖,如果依赖形成循环,go build 会报错。

没有引用的包会编译错误,但有时候导入包是为了对包级别的变量执行初始化表达式求值,并执行init函数。为此可以使用以下形式避免编译错误。这称为空白导入。

1
import _ "image/png"

包及其命名

  • 尽量使用简短名称
  • 尽可能保持可读性和无歧义,例如imageutilioutil
  • 使用统一形式,例如使用复数来避免与关键字冲突,例如byteserrors

go 工具

工作空间组织

GOPATH 工作空间,包含srcpkgbin三个子目录。src下是不同的包。

GOROOT发行版本的根目录,包含所有标准版,目录结构类似GOPATH

go get

go get 创建的目录是远程仓库的 clone,可以使用 git 来查看或更新。

添加-u 开关会确保 go get 将其以及依赖性更新到最新版本,然后再构建和安装,反之已经存在于本地的包不会更新。

vendor 目录用于构建关于所有必须依赖的本地副本,1.5 之后就不需要了。

包的构建

go build 编译每一个命令行参数中的包,如果是一个库则舍弃。构建所有需要的包以及他们所有的依赖性,然后舍弃除了最终可执行程序置为的所有编译后的代码。

go install 类似 go build,区别是它会保存每一个包的编译代码和命令,保存在$GOPATH/pkg 目录中。可执行文件保存在$GOPATH/bin 目录中。

一个文件包含操作系统或者处理器体系结构名称,如(net_linux.go、asm_amd64.s),go 工具只会在构建指定规格目标文件的时候才进行编译。

构建标签的特殊注释,提供更细粒度的控制。

1
// +build linux darwin

注释在包的声明之前,go build 只会在构建 Linux 或 Mac 系统应用时才会对它进行编译。

// +build ignore 表示任何时候都不要编译这个文件。

go doc xxx,查看指定内容的声明和整个文档注释,xxx可以是包名、方法、包成员。

比较长的包注释可以使用单独的注释文件,通常叫doc.go

包的导入路径包含internel的包称为内部包,内部包只能被以internel目录的父目录为根目录的树中。

go list github.com/go-sql-driver/mysql 判断一个包是否在工作空间中,存在输出它的导入路径。

go list ... 枚举一个 Go 工作空间的所有包。

go list gopl.io/ch3/... 指定子树的所有包。

go list ...xml... 某个具体主题

go list -json hash 已 json 格式输出包的完整元数据。

go list -f 可以定制输出格式

测试

_test.go结尾的文件不是go build命令编译的目标,是go test编译的目标。

在测试文件中,有三个函数需要特殊对待:

  • Test前缀函数,是功能测试函数
  • Benchmark 前缀函数,是基准测试函数
  • Example 前缀函数,是示例函数

go test 工具扫描测试文件寻找特殊函数,然后生成一个临时的main包来调用它。

go test 默认以当前目录所在包作为参数

不要再测试代码中调用log.Fatal或者os.Exit函数。

选项

  • -v 可以输出每个测试用例名称和执行时间。
  • -run="regx" 指定一个正则表达式,只需要匹配名称的测试函数。
  • -bench=. 指定一个正则表达式,执行匹配的Benchmark 函数。
  • -benchmem 在报告中包含
  • -cpuprofile=cpu.out 执行 CPU 性能剖析。
  • -blockprofile=block.out 执行阻塞性能剖析,识别出阻塞最久的操作。
  • -memprofile=mem.out 堆性能剖析,识别出分配最多内存的语句。

Test 函数

函数签名如下

1
2
3
func TestName(t *testing.T) {

}

t.Errorf 输出失败的测试用例信息

t.Fatalt.Fatalf 输出失败的测试用例信息,并且终止测试

外部测试包

Go 语言不允许循环引用,但是为了测试有可能会造成循环引用。为了避免该情况,将需要用到的其他引用的函数定义在外部测试包中,该包声明以_test为后缀。一般以export_test.go 命名。

覆盖率

go test -cover 包名 输出测试覆盖率的汇总信息

go test -run=Coverage -convermode=count -coverprofile=c.out 包名 输出汇总信息,并且输出覆盖数据收集。

go tool cover -html=c.out 处理生成的日志,生成一个 HTML 报告。

Benchmark 函数

函数签名如下

1
2
3
func BenchmarkName(b *testing.B) {

}

*testing.B 大多数方法同*testing.T,额外增加一些与性能检测相关方法

  • N,整型成员,用来指定被检测操作的执行次数。

性能剖析

go test -run=NONE -bench=ClientServerParallelTLS64 -cpuprofile=cpu.log net/http

go tool pprof -text -nodecount=10 ./http.test cpu.log

Example 函数

示例函数既没有参数也没有结果,主要有三个目的。

  • 作为文档,godoc 会将函数以及相关的示例函数关联起来。
  • 执行测试
  • 提供手动实验代码

反射

反射由reflect包提供,定义了两个重要类型,TypeValue

reflect.TypeOf 接受任何参数,将接口中的动态类型以reflect.Type形式返回。包含的是具体类型。

reflect.ValueOf 接受任何参数,将接口中的动态值以reflect.Value`形式返回。包含的是具体值。

尽管 Go 有无限多类型,但是类型的分类只有少数几种。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func formatAtom(v reflect.Value) string {
switch v.Kind() {
case reflect.Invalid:
return "invalid"
case reflect.Int, reflect.Int8, reflect.Int16,
reflect.Int32, reflect.Int64:
return strconv.FormatInt(v.Int(), 10)
case reflect.Uint, reflect.Uint8, reflect.Uint16,
reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return strconv.FormatUint(v.Uint(), 10)
// ...floating-point and complex cases omitted for brevity...
case reflect.Bool:
return strconv.FormatBool(v.Bool())
case reflect.String:
return strconv.Quote(v.String())
case reflect.Chan, reflect.Func, reflect.Ptr, reflect.Slice, reflect.Map:
return v.Type().String() + " 0x" +
strconv.FormatUint(uint64(v.Pointer()), 16)
default: // reflect.Array, reflect.Struct, reflect.Interface
return v.Type().String() + " value"
}
}

reflect.Value 相关方法介绍

  • Len() 返回 slice 或者数组中元素个数,Index(i) 返回第 i 个元素,类型为reflect.Value
  • NumField() 返回结构体的字段数,Field(i)返回第 i 个字段,类型为reflect.Value
  • MapKeys 返回元素类型为reflect.Value的slice,每个元素都是一个 map 的键,MapIndex(key) 返回 key 对应的值。
  • Elem() 返回指针指向的变量、接口动态值,也是refect.Value类型
  • IsNil 可以检测空指针、 接口是否为空。
  • CanAddr 判断变量是否可寻址。
  • CanSet 判断一个 reflect.Value是否可寻址且可更改

####如何修改变量的值

通过反射修改一个变量有两种方式,一是获取该变量的地址,通知指针修改;二是通过可寻址的 reflect.Value 来更新变量。

1
2
3
4
5
6
x := 2
d := reflect.ValueOf(&x).Elem()
px := d.Addr().Interface().(*int)
*px =3

d.Set(reflect.ValueOf(4))

在不可寻址的 reflect.Value调用Set方法或者类型不匹配都会造成程序崩溃。

Set还有一些基本类型特化的变种:SetIntSetUintSetStringSetFloat

反射可以读取到未导出的结构字段值,但是不能更新。

标准库

flag

Go 语言标准库中有一个代码包专门用于接收和解析命令参数,这个代码包叫做flag

1
2
3
4
5
6
7
8
9
10
11
12
import (
"flag"
"fmt"
)
var name string
func init() {
flag.StringVar(&name, "name", "everyone", "The greeting object.")
}
func main() {
flag.Parse()
fmt.Printf("Hello , %s!\n", name)
}

使用 flag.Usage

1
2
3
4
5
6
7
flag.Usage = func() {

fmt.Fprintf(os.Stderr, "Usage of %s:\n", "question")

flag.PrintDefaults()

}

该代码要放在flag.Parse()之前,当 运行go run demo.go --help时,Fprintf的内容将输出到终端上。

flag.CommandLine

我们调用flag包的一些函数的时候,实际上是在调用flag.CommandLine变量的对应方法。对flag.CommandLine重新赋值可以更深层次的定制当前命令源码文件的参数说明。 在init()函数开始处添加以下代码:

1
2
3
4
5
flag.CommandLine = flag.NewFlagSet("", flag.ExitOnError)
flag.CommandLine.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage of %s:\n", "question")
flag.PrintDefaults()
}

好处是保留了定制命令参数容器的灵活性,且不会影响全局变量flag.CommandLine

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main
import (
"flag"
"fmt"
"os"
)
var name string
var cmdLine = flag.NewFlagSet("question", flag.ExitOnError)
func init() {
cmdLine.StringVar(&name, "name", "everyone", "The greeting object.")
}
func main() {
cmdLine.Parse(os.Args[1:])
fmt.Printf("Hello , %s!\n", name)
}

sort

sort.Interface 接口提供三个方法,任何实现这三个方法的类型都可以使用sort.Sort函数进行排序。

1
2
3
4
5
type Interface interface {
Len() int //序列长度
Less(i, i int) bool // 比较两个元素
Swap(i, j int) // 交换两个元素
}

sort.Strings 可直接对字符串 slice 排序。

sort.Reverse 逆序排序

http

1
2
3
4
5
type Handler interface {
ServeHTTP(w ResponseWriter, r *Request)
}

func ListenAndServe(address string, h handler) error

一个简单的 http 服务器

1
2
3
4
5
6
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
_, _ = fmt.Fprint(w, "hello world")
})
_ = http.ListenAndServe(":8080", nil)
}
  • ResponseWriter.WriteHeader :写返回值
  • Request.URL.Query() 获取请求参数,返回类型multimap
  • Request.URL.Path 请求路径

http提供了一个请求多工转发器ServeMux,用来简化 URL 和处理程序之间的关联。一个ServeMux把多个http.Handler组合成单个http.Handler

1
2
3
4
5
6
func main() {
mux := http.NewServeMux()
mux.Handle("/list", http.HandlerFunc(handler1))

http.ListenAndServe("localhost:8000", mux)
}

http包提供了一个全局的ServeMux实例DefaultServeMux,以及包级别的注册函数http.Handlehttp.HandleFunc

io

为了区别文件结束和其他错误操作,io包下有个不同的错误——io.EOF

注意 很多文件系统中,写错误往往不会立即返回,而是推迟到文件管理的时候,如果不检查关闭操作的结果,就会导致一系列的数据丢失。