例子:
package main // 包声明
import "fmt" // 导入声明
const pi = 3.14 // 包级别的声明
func main() { // main 同样是包级别的声明
var f = 123.456 // 局部声明
fmt.Printf("f = %g\n", f)
}
parseRequestLine
HTMLEscape
,escapeHTML
而不是escapeHtml
Go有25个关键字:
break | default | func | interface | select |
case | defer | go | map | struct |
chan | else | goto | package | switch |
const | fallthrough | if | range | type |
continue | for | import | return | var |
var name type = expression
= expression
部分可以省略,但不能两者同时省略。expression
推断类型。= expression
,则该变量初始为0值: 一次声明多个变量:
var i, j, k int // int, int, int
var b, f, s = true, 2.3, "four" // bool float64 string
多个变量声明为函数的返回值:
var f, err = os.Open(name) // os.Open returns a file and an error
如果声明了一个变量,却没有使用它,则会报错。
name := expression
,name的类型根据expression推断。i, j := 0, 1
多个变量声明为函数返回值:
f, err := os.Open(name)
多变量声明时不必所有变量都是新创建的变量,但必须至少有一个是新创建的变量。多变量声明中已经创建过的变量视为赋值:
// 假设in,err和out都没有被声明过
in, err := os.Open(infile) // ok, 声明in和err
//...
out, err := os.Create(outfile) // ok, 声明out,给err赋值
// 假设f,err没有被声明过
f, err := os.Open(infile)
//...
f, err := os.Create(outfile) // 编译错误,没有新变量被创建
=
=
则组成复合赋值运算符,例如:+=
,*=
数值变量可以使用自增和自减运算符是自身的值加1或减1。没有前置自增。
v := 1
v++ // same as v = v + 1; v becomes 2
v-- // same as v = v - 1; v becomes 1 again
多重赋值允许一次赋值多个变量:
i, j, k = 2, 3, 5
x, y = y, x // 交换两个变量的值
若函数或操作符返回多个值,则在赋值左侧必须用多个变量,个数必须与返回值的个数一致:
f, err = os.Open("foo.txt") // function call return two values
如果不想用多个函数返回值中的某个或某几个,则可以用_
代替:
_, err = io.Copy(dst, src)
*int
用&取一个变量的地址。用*访问指针所指的变量,例如:
x := 0
p := &x
*p = 1 // x now is 1
可以返回函数内的局部变量的地址:
var p = f()
func f() *int {
v := 1
return &v // ok
}
new函数创建一个匿名对象并返回它的地址,这个对象的初始值设为0值:
p := new(int) // p is *int, *p is 0
返回局部变量的地址,与返回new创建的变量等效:
func newInt() *int {
return new(int)
}
// 等效于:
func newInt() *int {
var dummy int
return &dummy
}
空标识 _
,用于在语法上需要一个变量,但是逻辑上不需要变量的时候。比如 range for 中不需要的变量, 函数返回值列表中不需要的返回值。
a := [3]string{
"hello","go","world"}
for _, v := range a { // 不需要索引值,以_代替
fmt.Print(v, " ")
}
_, x := func() (int, int) { // 不需要第一个返回值,以_代替
return 1, 2
}()
const pi = 3.14159
声明多个常量用括号括起:
const (
e = 2.718281
pi = 3.14159
)
常量可用作数组的维度:
const len = 4
var p [len]byte
const声明一组常量时除第一常量以外可以分配默认值,该值与上一个常量一致:
const (
a = 1
b // b is 1
c = 2
d // d is 2
)
在声明一组常量时,可以使用iota 组成的表达式给第一个常量赋值,第一个常量之后的每一个常量都应用这个表达式,但iota的值从0开始,每到下一个常量便加一,通常以这种方式赋值的常量又叫枚举。
const (
a = iota // a is 0
b // b is 1
c // c is 2
d // d is 3
)
const (
flag1 = 1 << iota // flag1 is 1 (1 << 0)
flag2 // flag2 is 2 (1 << 1)
flag3 // flag3 is 4 (1 << 2)
flag4 // flag4 is 8 (1 << 3)
)
没有冠以类型的常量为无类型常量,无类型常量在被赋予类型之前要比有类型常量有更大的精度(通常精度可高达256位),并且能参与精度更大的计算:
const (
a = 1 << (100 + iota)
b // b为100位的整数,已经超过了最大的uint64的值
c // c为101位的整数
)
fmt.Printf("%d", c/b) // print 2
无类型常量根据常量的字面形式,分为无类型整数,无类型浮点数,无类型bool值,无类型rune,无类型复数:
const (
a = 100 // 无类型整数
b = 1.0 // 无类型浮点数
c = true // 无类型bool值
d = '\u1234' // 无类型rune
e = 3 + 2i // 无类型复数
)
类型 | 符号 | 位数 | 范围 | 说明 |
---|---|---|---|---|
int8 | 有符号 |
8 | −2n−1−1∼2n−1−1 |
|
int16 | 16 | |||
int32 | 32 | |||
rune | 32 | rune是int32的别名, 用来表示Unicode码点(code point)。 |
||
int64 | 64 | |||
uint8 | 无符号 |
8 | 0∼2n−1 |
|
uint16 | 16 | |||
uint32 | 32 | |||
uint64 | 64 | |||
int | 有符号 | ? |
? |
不同的编译器根据不同的平台设置不同的大小, 以达到最高效的利用硬件。 |
uint | 无符号 | |||
uintptr | 无符号 | uintptr是一个无符号整型, 仅用来兼容低级语言(比如C)。 |
可用于整型的操作符和优先级如下(优先级从高到低,每行优先级相同):
* | / | % | << | >> | & | &^ |
+ | − | | | ^ | |||
== | != | < | <= | > | >= | |
&& | ||||||
|| |
前两行的操作符都有一个对应的复合赋值操作符,比如+
对应+=
+
-
*
/
可用于整型、浮点型、复数。==
!=
<
<=
>
>=
可应用于两个类型相同的基基本类型(整型、浮点型、字符串),结果为bool类型。+
-
亦可作为一元运算符,表示正负。Go提供了如下的位运算符:
& | 按位与 |
| | 按位或 |
^ | 异或 |
&^ | 与非 |
<< | 左位移 |
>> | 右位移 |
在使用fmt.Printf()打印整型时,10进制、8进制、16进制分别用 %d %o %x(或%X表示大写) :
o := 0123
fmt.Printf("%d %[1]o %#[1]o\n", o) // 83 123 0123
x := int64(0xABC)
fmt.Printf("%d %[1]x %#[1]x %#[1]X\n", x) // 2748 abc 0xabc 0XABC
#
表示打印8进制或16进制开头的字符打印rune类型用 %c(不带引号)或者 %q(带引号),打印出的是Unicode字符:
ch := '中'
ch2 := '国'
fmt.Printf("%d %[1]c %d %[2]q\n", ch, ch2) // 20013 中 22269 '国'
类型 | 精度 | 最大值 | 最小值 |
---|---|---|---|
float32 | 6位 | math.MaxFloat32 | math.MinFloat32 |
float64 | 15位 | math.MaxFloat64 | math.MinFloat64 |
特殊值 | 意义 | 例子 |
---|---|---|
+Inf | 正无穷 | 5.0/0.0 |
-Inf | 负无穷 | -5.0/0.0 |
NaN | Not a Number | 0/0 |
==
和 !=
> >= < <= == !=
),逻辑非:!
&&
)和逻辑或(||
)连在一起,并产生短路行为(貌似所有的语言都是如此,至少我所知的是如此)。+
操作符以拼接字符串。支持+=
以拼接新的字符串并赋值给变量自身。s[0]='v'
得到编译错误。正是因为字符串不能改变,切片所返回的字符串可以和原字符串共用同一段内存,以提高效率。常用的转义字符:
字符 | 意义 |
---|---|
\n | 换行 |
\r | 回车 |
\t | 制表符 |
\’ | 单引号 |
\” | 双引号 |
\\ | 反斜线 |
只有小于256的单个16进制数可以转义为一个rune,否则只能通过/u或/U的方式来转换:
var a rune = '\x41' // ok
a = '\xe4\xb8\x96' // error
a = '\u4e16' // ok
unicode/utf8包里提供了UTF-8和rune相关的函数:
DecodeRuneInString接受一个字符串,并返回该字符串的第一个字符的rune值以及表示这个rune需要的byte数量:
s := "你好"
r, size := utf8.DecodeRuneInString(s)
fmt.Printf("%q is %d bytes", r, size)
// 打印: '你' is 3 bytes
RuneCountInString返回字符串中rune的个数
s := "你好"
fmt.Printf("rune count: %d\nbyte count: %d",
utf8.RuneCountInString(s), len(s))
// 打印:
// rune count: 2
// byte count: 6
range for 自动解码UTF-8字符串为tune序列:
s := "你好"
for i, r := range s {
fmt.Printf("%d: %q\n", i, r)
}
// 打印:
// 0: '你'
// 3: '好'
[num]T
,其中num是元素个数,T是类型,例如:[3]int
表示元素个数为3的int数组。数组的声明:
var a [3]int // 创建一个由3个int值组成的数组
数组初始化
用数组字面值初始化:
var a [3]int = [3]int{1, 2, 3}
未被显式初始化的元素被隐式初始化为0值:
var a [3]int = [3]int{1, 2} // a[2] is 0
var b [3]int // 所有元素都为0
若以 ...
指定数组长度,则由初始化列表中元素的个数决定数组真实长度:
q := [...]int{1, 2, 3} // q is [3]int
初始化时可以指定下标:
a = [...]string{1:"hello", 3:"world"} // a[2] is empty string
使用内置函数len()返回数组的长度。
a := [3]int{1,2,3}
fmt.Println(len(a)) // 输出3
使用下标运算符[]
获取数组中某个元素的值。下标从0开始。
a := [3]int{1, 2, 3}
fmt.Println(a[2]) // 获取第二个元素的值,打印: 3
a[2] = 4 // 赋值给第二元素
fmt.Println(a[2]) // 打印: 4
两个长度相同,元素类型相同的数组视为同类型数组,只有同类型数组才可以进行比较
a := [3]int{1, 2, 3}
b := [3]int{1, 2, 3}
c := [2]int{1, 2}
d := [3]float32{0., 1., 2.}
fmt.Println(a == b) // ok,打印true
fmt.Println(a == [3]int{1, 2, 4}) // ok, 打印false
fmt.Println(a == c) // 编译错误, 不能比较[3]int和[2]int
fmt.Println(a == d) // 编译错误, 不能比较[3]int和[3]float32
Go可以声明一个空数组,但是我不知道他能干什么:
a := [0]int{} // I really want to known what an empty array can do
[]T
表示,其中T是类型。例如[]int是一个int数组的切片。切片和数组紧密的联系在一起。切片是一个轻量级的数据结构,可以访问数组中的子序列。这个数组叫做基数组。切片有三个属性:指针,长度,容量。指针指向切片能访问到的基数组中的第一个元素。长度为切片中元素的数量。容量为切片的第一个元素到基数组的最后一个元素的长度。内置函数len()返回切片长度。内置函数cap()返回切片容量。
a := [...]int{1, 2, 3, 4, 5, 6} // 创建一个[6]int数组
s := a[1:4] // s is [2 3 4]
fmt.Println(len(s)) // 打印3
fmt.Println(cap(s)) // 打印5
切片不能访问超出其长度的元素(下标必须小于长度),否则运行时报错:
a := [...]int{1, 2, 3, 4, 5, 6} // 创建一个[6]int数组
s := a[1:4] // s is [2 3 4]
fmt.Println(s[3]) // runtime error: index out of range
多个切片可以有同一个基数组,多个切片可能有交集。
切片操作符s[i:j]( 0≤i≤j≤cap(s) )创建一个以s为基数组从i到j(不包含j)的切片。如果省略i,则i为0;如果省略j,则j为len(s);都省略则返回整个数组的切片。
a := [...]int{1, 2, 3, 4, 5, 6} // 创建一个[6]int数组
s := a[1:4] // s is [2 3 4]
s = a[:3] // 相当于s = a[0:3],s is [1 2 3]
s = a[3:] // 相当于s = a[3:6],s is [4 5 6]
s = a[:] // 相当于s = a[0:6],s is [1 2 3 4 5 6]
由于切片包含指向基数组的指针,因此可以通过切片改变数组的内容。
a := [...]int{1, 2, 3, 4, 5, 6} // 创建一个[6]int数组
s := a[1:4] // s is [2 3 4]
s[1] = 7 // a 变成 [1 2 7 4 5 6]
切片的0值为nil。
var s []int
fmt.Println(s == nil) // true
make()创建一个切片。
make([]T, len)
会在内部创建一个长度为len的数组,并返回整个数组的切片make([]T, len, cap)
会在内部创建一个长度为cap的数组,并返回前len个元素组成的切片。append追加元素到切片末尾:
a := []int{1, 2, 3}
a = append(a, 4) // a is [1 2 3 4]
a = append(a, 5, 6) // a is [1 2 3 4 5 6]
b := []int{7, 8, 9}
a = append(a, b...) // a is [1 2 3 4 5 6 7 8 9]
copy拷贝一个切片到另一个切片:
c := []int{1, 2, 3}
d := make([]int, 3, 6)
copy(d, c) // d is [1 2 3]
sort包提供了Strings(),Ints()等函数可以对切片进行排序:
import "sort"
e := []int{3,4,1}
sort.Ints(e)
fmt.Println(e) // 打印 [1 3 4]
两个切片不能比较,切片只能和nil比较
a := [...]int{1, 2, 3, 4, 5, 6}
s, k := a[1:4], a[1:4]
if s == k { // error: 两个切片不能比较
...
}
if s == nil { // ok, 结果为false
...
}
==
比较的类型。map中K值不能重复。K值在map中无序。创建:
m := make(map[string]int) // 用内置函数make创建
n := map[string]int{ // 用map字面值创建
"a":1,
"b":2,
}
插入/更新:
n["c"] = 3
n["d"] = 4
删除:
delete(n, "c") // 使用内置函数delete删除
遍历:
// 使用range for遍历
for x,y := range n {
fmt.Printf("%s %d\n", x, y)
}
不能取元素地址:
_ = &n["b"] // error
判断某个键是否在map中:
age, ok := ages["bob"] // 如果不存在键"bob",则ok为false
type name underlying-type
类型声明定义的两个类型即使基础类型相同,也是不同的类型,不能应用于算术操作符和比较操作符:
// Width 和 Count 是两个不同的类型,即使基础类型一致
type Width int
type Count int
// 声明两个变量
var w Width = 12
var c Count = 12
// ...
if (w == c) // 错误,w和c类型不一致
// ...
可以用typeName(underlyingType)的形式从基础类型转换为新类型:
type Radius float64
r = Radius(12.0)
基础类型相同的两个变量可以互相转换;如果两个指针指向的变量的类型的基础类型相同,则它们可以互相转换:
type Radius float64
type Length float64
len := Length(0)
r := Radius(len) // ok, Length to Radius
plen := &len
pr := (*Radius)(plen) // ok, *Length to *Radius
结构体的声明:
type Point struct {
X int
Y int
}
多个相邻的字段如果类型相同可以写在一起。
type Size struct {
Width, Height int
}
访问结构体的字段用点操作符。
p := Point{1, 2}
p.X = 5
可以取字段的地址。指针访问字段时用点操作符。
pp := &p
pp.X = 6
-可以用new创建结构体,new的返回值为创建的结构体的指针:
ps := new(Size)
结构体的类型由字段的类型和顺序确定。结构体若所有字段类型都是可比较的类型,则该结构体也是可比较的类型。 两个结构体变量类型和字段值都相等则相等,否则不相等。
结构体中可以有其他结构体字段:
type Point struct {
X,Y int
}
type Size struct {
Width, Height int
}
type Rect struct {
point Point
size Size
}
结构体字面值为结构体类型后面接大括号,按顺序写字段的值,用逗号分隔。
p := Point{1, 2}
也可以指定字段名称。若指定了名称, 则字段的顺序无关紧要:
p2 := Point{Y:2, X:1}
可以在结构体字面值之前加&,以返回用字面值创建的结构体的地址。
pp2 := &Point{1, 2}
// 相当于:
pp2 := new(Point)
*pp2 = Point{1, 2}
一个字段的声明中仅有类型,而没有字段名称,该字段称为匿名字段。
type Point struct {
X,Y int
}
type Size struct {
Width, Height int
}
type Rect struct {
Point // Point 为匿名字段
size Size
}
访问匿名字段是可以跳过或不跳过中间的类型:
var r Rect
r.X = 1 // ok,跳过中间类型
r.Point.Y = 2 // ok, 不跳过中间类型
r.size = Size{3, 4}
在写字面值时匿名字段时不能省略中间类型:
r = Rect{1, 2, 3, 4} // error,不能省略中间类型。
r = Rect{Point{1, 2}, Size{3, 4}} // ok
r = Rect{size:Size{3,4}, Point:Point{1,2}} // ok
结构体中字段的后面可以跟一个字符串字面值,用来表示这个字段的额外信息(metadata,元数据),这种标签叫做字段标签(field tag),通常是由一对或多对key:"value"
组成的,中间用空格分隔,由于value是以双引号括起来的字符串,为了避免过多转义(\"
),字段标签通常以原始字符串表示。例如以下结构体给X定义了一个结构体标签,有两对key:"value"
s:
type Point struct {
X int `desc:"x position" author:"Cynhard"`
Y int
}
用反射机制获取结构体字段标签的值:
如果reflect.Type的类别(Kind())是一个结构体(reflect.Struct),则reflect.Type就表示一个结构体类型。reflect.Type的FieldByName()方法返回一个类型为reflect.StructField的结构体,这个结构体记录了字段的所有信息,其中reflect.StructField.Tag的类型为reflect.StructTag,是一个基类型为string的类型声明,这个类型有两个方法Get和Lookup用来根据字段标签中key的值获取value,Lookup返回一个额外的bool值,表示key是否存在,见下例:
package main
import (
"fmt"
"reflect"
)
type Point struct {
X int `desc:"x position" author:"Cynhard"`
Y int `desc:"y position"`
}
func main() {
pt := Point{1, 2}
tp := reflect.TypeOf(pt)
if tp.Kind() == reflect.Struct {
if fieldX, found := tp.FieldByName("X"); found {
fmt.Printf("%s created by %s\n",
fieldX.Tag.Get("desc"), fieldX.Tag.Get("author"))
}
if fieldY, found := tp.FieldByName("Y"); found {
if desc, found := fieldY.Tag.Lookup("desc"); found {
fmt.Printf(`desc of Y is "%s"`+"\n", desc)
}
}
}
}
结构体字段标签通常用在数据交换中从字符串转为结构体时使用(比如json字符串转结构体)。
Go不要求语句后面接分号,除非一行中有两条语句,这两条语句中间需要加分号。实际上在Go语言中,换行符会自动转换成分号,除非换行符前有明确的证据说明不应当将换行视为分号。因此标识函数体开始的 { 必须跟在函数签名之后:
func main() // 错误, 解析为 func main();
{
}
func main() { // ok, 因为 "func main() {" 最后的左花括号表示函数体的开始,
} // 因此后面的换行符不会解析成分号
x := 2 // 解析成 x := 2;
* 3 // error here
x := 2 * // ok,乘号是双目运算符,需要有右操作数,
3 // 因此换行符不会解析成分号, 相当于 x := 2 * 3
if有一个可选的else分支,在判断条件为假的时候执行。
if condition {
// ...
} else {
// ...
}
可以在if语句里声明变量,这个变量的作用域是所有分支:
if x := 3; x < 0 {
fmt.Println("x < 0")
} else {
fmt.Println("x >= 0")
}
if语句可以嵌套:
if x := 3; x < 0 {
fmt.Println("x < 0")
} else if x > 0{
fmt.Println("x > 0")
} else {
fmt.Println("x == 0")
}
switch是多路分支,用表达式的值和每一个case的值按从上到下的顺序比较,如果相等,则执行相应的case后退出switch。如果没有找到对应的case,则执行default。
switch expression {
case value1:
// ...
case value2:
// ...
...
default:
// ...
}
switch可以没有表达式:
x := 3
switch {
case x > 0:
fmt.Println("x > 0")
case x < 0:
fmt.Println("x < 0")
default:
fmt.Println("x == 0")
}
第一种形式和C/C++的for一样。for语句不需要将 initialization; condition; port
用括号括起来。但是循环体必须要有花括号括起来,并且左花括号必须在post后面。
for initialization; condition; post {
}
第二种相当于C/C++的while:
for condition {
}
第三种是一个无限循环,相当于C/C++的while(true),用break或return来终止无限循环。
for {
}
第四种是range for,可以用来遍历如string,数组,切片,map等的数据类型。range 在每一次迭代中返回两个值,第一个是索引,第二个是索引对应的值。
// 遍历数组
a := [3]string{
"hello","go","world"}
for i, v := range a {
fmt.Println(i, ":", v)
}
// 遍历map
m := map[string]string{
"Name":"Cynhard", "Address":"China"}
for k, v := range m {
fmt.Println(k, ":", v)
}
Go支持带标签的break和continue,以从内层循环跳转至外层循环:
func main() {
Outer:
for j := 0; j < 10; j++ {
for i := 0; i < 10; i++ {
if j == 1 {
break Outer
}
if i == 5 {
continue Outer
}
fmt.Print(i, " ")
}
}
}
// 输出结果:0 1 2 3 4
基本形式:
func name(parameter-list) (result-list) {
body
}
Go函数可以返回多个值。
func area(x int, y int) (int, bool) {
if x < 0 || y < 0 {
return 0, false
}
return x*y, true
}
相邻参数若类型相等,则可以合并。
func doSomething(x, y int) (a, b string) {
// ...
}
带返回值列表的函数必须包含return语句。
func area(x, y int) (area int, ok bool) {
if x < 0 || y < 0 {
return 0, false
}
area = x * y
ok = true
return // 必须写return,即便已经到了函数结尾
}
函数体里如果没有用到某个参数,则可以用_
代替名称。
func doSomething(x int, _ int) int {
}
函数的签名由参数列表中的类型和顺序以及结果列表中的类型和顺序构成。函数的类型由签名决定。
函数可以作为值来使用。可以赋值给变量或者从函数返回。
func double(x int) int { return 2 * x }
func getDouble() func(int) int { return double } // 函数作为返回值返回
// ...
f := double // 将函数赋值给变量
f(3) // 通过变量调用函数
f = getDouble() // 将getDouble()返回的函数赋值给变量
匿名函数可以访问整个词块的变量。在调用匿名函数时对其用到的变量重新求值。
func increment() func() int {
var x int
return func() (y int) { x++; return x }
}
inc := increment()
fmt.Println(inc()) // 1
fmt.Println(inc()) // 2
fmt.Println(inc()) // 3
在使用匿名函数时,须特别注意在for词块中引入的循环变量。如果在匿名函数里保存了这个变量,则会造成意想不到的结果,例如,下面的程序试图将一个数组的元素全部临置成0,随后再恢复原来的值:
func main() {
a := [3]int{1, 2, 3}
var recs []func()
for i, v := range a {
a[i] = 0
recs = append(recs, func() {
a[i] = v // 错误!
})
}
// do something
// 试图恢复原来的值
for _, rec := range recs {
rec()
}
fmt.Println(a) // 打印 [0 0 3]
}
最后得到了错误的结果。原因如下:for循环引入了一个新的词块,在这个词块中声明了i和v。所有在循环体里创建的匿名函数都捕捉了这两个变量本身(变量的地址),而不是捕捉了这两个变量的值。因此在试图恢复数组的值时,每个匿名函数都是以第一个for循环结束时i和v的值(2和3)带入。相当于执行了3次a[2]=3。为了解决这个问题,通常的做法是在循环体内部声明一个同名变量作为拷贝,再将这个新声明的变量带入匿名函数。将上面的第一个for循环修改如下:
for i, v := range a {
i, v := i, v // 声明同名变量,拷贝i和v的值
a[i] = 0
recs = append(recs, func() {
a[i] = v
})
}
解决问题的关键点是每个存在于匿名函数体之内的变量,都有其自己的地址。
另见:for陷阱之defer、for陷阱之go
参数个数可变的函数为变参函数,最后一个参数的类型前加...
,实参类型为切片。
func max(nums ...int) (int, bool) {
if (len(nums) == 0) {
return 0, false
}
max := nums[0]
for _, val := range nums[1:] {
if (val > max) {
max = val
}
}
return max, true
}
fmt.Println(max()) // 0 false
fmt.Println(max(1)) // 1 true
fmt.Println(max(2, 3)) // 3 true
fmt.Println(max(4, 5, 6)) // 6 true
defer语句应尽可能的紧跟在开辟资源的语句后面。
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close() // 延迟调用Close()紧跟在Open()之后
不要在循环体里用defer清理资源,考虑以下程序:
package main
import (
"path/filepath"
"os"
)
var filenames []string
func main() {
processDir(`G:\projects`)
}
func processDir(root string) error {
// filepath.Walk(rootDir, walkFn)遍历rootDir下的所有
// 文件和文件夹,每遇到一个文件或文件夹,都会调用walkFn
filepath.Walk(root, walkHandler)
// 遍历文件路径数组,打开文件,处理文件
for _, filename := range filenames {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close() // 危险!有可能耗尽内存!
// do something with file
// ...
}
return nil
}
func walkHandler(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// 如果是文件,则保存文件路径到数组
if !info.IsDir() {
filenames = append(filenames, path)
}
return nil
}
defer file.Close()
语句是在函数return
语句执行完毕后才会执行的,不会在for
循环体中执行。for
循环的每次遍历都打开一个文件句柄,但没有在循环体结束前关闭它。随着for
循环迭代次数的增加,有可能会耗尽内存。正确的做法是将for
循环体中的语句放在一个函数中:
func processFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close() // ok now
// do something with file
// ...
return nil
}
func processDir(root string) error {
// filepath.Walk(rootDir, walkFn)遍历rootDir下的所有
// 文件和文件夹,每遇到一个文件或文件夹,都会调用walkFn
filepath.Walk(root, walkHandler)
// 遍历文件路径数组,处理文件
for _, filename := range filenames {
processFile(filename)
}
return nil
}
当然,更简单的,也可以用闭包:
func processDir(root string) error {
// filepath.Walk(rootDir, walkFn)遍历rootDir下的所有
// 文件和文件夹,每遇到一个文件或文件夹,都会调用walkFn
filepath.Walk(root, walkHandler)
// 遍历文件路径数组,处理文件
for _, filename := range filenames {
err := func () error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close() // ok now
// do something with file
// ...
return nil
}()
if err != nil {
return err
}
}
return nil
}
另见: for陷阱之循环变量、for陷阱之Go
Go runtime
会自动调用内置函数panic
来报告错误。在发生panic时,defer函数会被调用,程序崩溃,打印错误。我们也可以自己调用panic
:
package main
func main() {
x := -1
if x < 0 {
panic("x can not less 0")
}
}
// Output:
// panic: x can not less 0
//
//goroutine 1 [running]:
//main.main()
// G:/projects/go/src/cynhard.com/tests/if/if.go:6 +0x6b
//exit status 2
内置函数recover用在defer函数内,当包含defer语句的函数调用panic(无论是runtime还是自己)时,recover结束当前panic状态,并返回panic值。包含defer语句的函数直接返回而不是在发生panic的地方继续执行:
package main
import "fmt"
func main() {
defer func() {
if p := recover(); p != nil {
fmt.Printf("error: %v", p)
}
return
}()
if x := 0; x == 0 {
panic("x can not be 0")
}
fmt.Println("After error")
}
// Output:
// error: x can not be 0
字符 | 意义 |
---|---|
%d | 十进制整数 |
%x, %o, %b | 16进制、8进制、2进制整数 |
%f, %g, %e | 浮点数 |
%t | 布尔值: true false |
%c | rune (Unicode码点) |
%s | 字符串 |
%q | 双引号括起来的字符串,或者单引号括起来的rune值 |
%v | reflect.Value |
%T | reflect.Type |
%% | 百分号 |
在函数名之前增加一个额外的参数。通过这种方式给这个类型增加一个方法:
type Point struct{ X, Y int}
// 给Point增加Report()方法
func (p Point) Report() {
fmt.Printf("{x:%d, y:%d}\n", p.X, p.Y)
}
函数名之前这个额外的参数叫做接收者(receiver),接收者只能有一个,不能有多个。
接收者可以为指针:
func (p *Point) MoveTo(q Point) {
p.X = q.X
p.Y = q.Y
return
}
方法调用:选择器后面加()
p := Point{1, 2}
pp := &Point{3, 4}
p.Report() // call Report()
pp.MoveTo(Point{5, 6}) // call MoveTo()
一个T类型的变量(不能是字面值)可以作为一个接受者为*T类型的方法的接收者;一个*T类型的变量(不能是字面值)也可以作为一个接收者为T类型的方法的接收者:
p := Point{1, 2}
pp := &Point{3, 4}
p.MoveTo(Point{5, 6}) // ok,相当于(&p).MoveTo(Point{5, 6})
pp.Report() // ok, 相当于(*pp).Report()
一个结构体可以直接调用匿名内嵌结构体的方法,而不必显式写出匿名内嵌结构体的名称:
type Circle struct {
Point // 匿名内嵌结构器
Radius int
}
c := Circle{Point{1,2}, 3}
c.Report() // ok, 相当于c.Point.Report()
方法也可以作为值使用:
p := Point{1, 2}
m := p.Report // 将方法作为值使用
m() // 相当于p.Report()
方法表达式表示为 T.Method或者(*T).Method,将方法表达式赋值给变量后,可用变量调用方法,接收者为第一个参数:
q := Point{1, 2}
m := Point.Report // 方法表达式
m(q) // 通过q调用
f := (*Point).MoveTo // 方法表达式,接收者为指针
p := Point{1, 2}
f(&p, &Point{3,4}) // 通过&p调用
接口声明的基本形式:
type InterfaceName interface {
signature1
signature1
}
接口里面声明的函数仅写函数签名。例如:
type A interface {
f1(int) int
f2(x, y int) (z int)
}
可以用已有的接口定义新的接口:
type Runner interface {
Run()
}
type Flyer interface {
Fly()
}
// RunFlyer相当于声明了Runner和Flyer中的所有方法,即Run()和Fly()
type RunFlyer interface {
Runner
Flyer
}
实际上Go没有实现接口这一语法。也不使用实现接口这个术语,而把这种术语叫做满足(satisfy)。如果一个类型拥有一个接口声明的所有方法,则称这个类型满足这个接口。如果一个类型满足一个接口,则可以将该类型的变量赋值给该接口变量。
// 声明Run方法
func (dog Dog) Run() {
fmt.Println(dog.name, "is running")
}
dog := Dog{
"Strong"}
// Dog 拥有Runner声明的所有方法,所以Dog满足Runner,
// 可以将Dog变量赋值给Runner变量
var r Runner = dog
r.Run()
如果*T声明了一个接口的所有方法,则只有*T满足这个接口,而不能认为T也满足这个接口。因此只能将*T变量赋值给接口,不能将T变量赋值给接口:
// 给*Dog增加一个Run方法
func (dog *Dog) Run() {
fmt.Println(dog.name, "is running")
}
func main() {
dog := Dog{
"Strong"}
var r Runner = dog // error: Dog没有声明Run()方法,不满足Runner
var r Runner = &dog // ok: *Dog声明了Run()方法,满足Runner
r.Run()
}
interface{}
,即没有声明任何方法的接口。因为空接口没有声明任何方法,也可以说任何类型都拥有空接口声明的所有方法,所以任何类型都满足空接口,因此可以将任何类型的变量赋值给空接口变量:
var any interface{}
any = true
any = 12.34
any = "hello"
类型断言形式:x.(T),其中x是能够生成接口的表达式,T为类型。
func getRunner() Runner {
return Dog{
"Little"}
}
var r Runner = Dog{
"Strong"}
r.(Dog) // 以接口变量进行断言
getRunner().(Dog) // 以返回接口的表达式进行断言
断言规则:
如果T是一个具体类型,则检测x的实际类型是否为T类型,如果是,则整个断言返回x的实际类型的值。如果不是,则运行时错误。说白了就是向下转型(从接口到对象的实际类型),如果失败则在运行时报错。
type Cat struct {
Name string
}
func (cat Cat) Run() {
fmt.Println(cat.Name, "is running")
}
var r Runner = Dog{
"Strong"}
d := r.(Dog) // ok, d的类型为Dog
c := r.(Cat) // 运行时错误! r的实际类型是Dog,不是Cat
如果T是一个接口类型,则判断x的实际类型是否满足T,若满足,则将断言结果转换为接口T的变量。说白了就是向下转型或横向转型,转型失败则在运行时报错。(为了节省空间,省略了不必要的换行)
package main
import "fmt"
type Runner interface { Run() }
type Flyer interface { Fly() }
type Talker interface { Talk() }
type RunTalker interface { Runner; Talker }
type Dog struct { Name string }
func (dog Dog) Run() { fmt.Println(dog.Name, "is running") }
func (dog Dog) Talk() { fmt.Println("Bark! Bark! Bark!") }
func main() {
var r Runner = Dog{
"Strong"}
r.Talk() // 错误!r是Runner接口,没有Talk()方法
rt := r.(RunTalker) // ok, 向下转型,可以通过rt调用RunTalker的方法
rt.Talk() // ok
t := r.(Talker) // ok, 横向转型,可以通过t调用Talker的方法
t.Talk() // ok
f := r.(Flyer) // 错误!r的实际类型是Dog,不满足Flyer
}
断言可以返回两个值,第二个值表示是否成功。用这种方式可以替代在运行时报错。
var r Runner = Dog{
"Strong"}
if f, ok := r.(Flyer); ok { // 根据转换结果执行相应的操作,替代运行时报错
f.Fly()
}
一个接口值可以存储多种具现类型值。比如一个interface{}可以存储int,string,bool等类型的值。我们可以将接口想象成类型的联合。类型断言可以将具现类从这个联合中型拆分出来,每种类型分别处理:
package main
import "fmt"
func main() {
var x interface{}
x = "123"
if _, ok := x.(int); ok {
fmt.Println("x is type of int")
} else if _, ok := x.(string); ok {
fmt.Println("x is type of string")
} else if _, ok := x.(bool); ok {
fmt.Println("x is type of bool")
} else {
fmt.Println("Some other type")
}
}
以这种方式来描述接口则称为差别联合类型(discriminated unions)。
像上面这种if-else的结构未免过于冗长,于是Go引入了另一种switch
语句,和一个关键字type
以达到简化的目的:
// 根据不同的类型,执行不同的操作。注意type是Go的关键字。
switch x.(type) {
case nil: // ...
case int, unit: // ...
case bool: // ...
case string: // ...
default: // ...
}
上面这种switch语句就称为类型多路选择(Type Switches)。
有时候我们需要在case里使用x的实际类型值,这时候只需再声明一个变量获取x.Type()的结果即可。这个变量通常和x同名:
package main
import "fmt"
func main() {
var x interface{}
x = 123
switch x := x.(type) { // 声明一个新的变量x
case int:
fmt.Println(x * 2)
case string: // ...
case bool: // ...
// ...
default: // ...
}
}
要创建一个goroutine,只需在语句前面加go即可。
func main() {
go func() { // 创建一个goroutine
fmt.Println("Hello, goroutine!")
}()
// 很可能无法输出"Hello, goroutine!",主goroutine结束会强制结束其它goroutines
}
用内置函数make()创建一个信道,第二个参数指定信道的容量。若不指定第二个参数或第二个参数为0,则为无缓冲信道;若第二个参数大于0则为有缓冲信道,参数的值即为缓冲区大小。
ch := make(chan int) // ch 类型为 'chan int',无缓冲信道
ch := make(chan int, 0) // ch为无缓冲信道
ch := make(chan int, 3) // ch位有缓冲信道,缓冲区大小为3
信道有两个主要操作:发送和接收。这两个操作都用<-
表示。<-
在信道的右侧表示发送,<-
在信道的左侧表示接收:
ch <- x // send
x = <-ch // receive and assign
<-ch // only receive
通过内置函数close()关闭信道:
close(ch)
在一个无缓冲区信道中执行发送操作时,发送操作阻塞,直到另一个goroutine在同一个信道中接收数据后,发送操作才能完成,goroutine继续执行。在一个无缓冲区信道中接收数据时,如果信道中没有可用的数据,则接收操作阻塞,直到另一个goroutine在同一个信道中发送数据,接收操作才会完成,goroutine继续执行。换句话说,无缓冲信道通信时是同步的。
func main() {
// 创建一个无缓冲区信道,因为只是用来通知主线程退出而并不传送实际数据,
// 因此这里声明信道类型为 chan struct{}
ch := make(chan struct{})
// 创建一个goroutine
go func() {
fmt.Println("Hello, goroutine!")
ch <- struct{}{} // 发送数据到信道
}()
// 若信道内无数据,则阻塞,直到可以接收到数据
// 这里忽略掉接收到的数据
<-ch
}
只发送或只接收的信道为单向信道。在函数声明时,如果信道为参数,则可指明信道是否单项。只发送则为chan<-,只接收则为<-chan:
func fa(out chan<- int) { // 只发送
}
func fb(in <-chan int) { // 只接收
}
内置函数cap()返回信道的容量。len()返回当前元素数量。
ch := make(chan int, 3)
ch <- 1
ch <- 2
fmt.Println("cap:", cap(ch)) // cap: 3
fmt.Println("len:", len(ch)) // len: 2
考虑以下用协程打印0到9的程序:
package main
import "fmt"
func main() {
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
ch := make(chan struct{})
for _, v := range s {
go func() {
fmt.Println(v) // 错误
ch <- struct{}{}
}()
}
// 等待所有的协程结束
for range s {
<- ch
}
}
程序的输出并非0到9,而是会有若干重复。这个错误与for陷阱之循环变量类似,都是在闭包里捕获了循环变量。在这个例子里,for循环的每一次迭代都在开启了一个协程后马上进入下一次迭代,开启另一个协程,这时上一个协程可能还没来得及运行,但是捕获的循环变量的值却已经改变,导致了多个协程处理同一个值的情况。解决方法是在闭包里避免捕获循环变量,因为Go函数参数都是值传递,因此可以给闭包增加一个参数,并将循环变量作为实参传递:
for _, v := range s {
go func(v int) {
fmt.Println(v) // ok, v是闭包的形参,值与循环变量一致
ch <- struct{}{}
}(v) // 将循环变量作为实参传入
}
另见:for陷阱之循环变量、for陷阱之defer
单线程中多个sockets在通信时,如果一个socket阻塞,则其他socket也得不到处理。为了处理这种情况,引入了select函数。类似的,在Go中假设一个goroutine中需要与多个信道通信(发送,或接收),则会发生一个信道阻塞,则其它信道(即便可以接收或发送数据)也阻塞的情况。为了处理这种情况,Go引入了select语句。一个select语句可以同时监听多个信道状态,在可以处理数据时,则执行相应的流程:
select {
case <-ch1: // 如果ch1可以接收数据,则接收数据(这里丢弃),并执行该case
// ...
case x := <-ch2: // 如果ch2可以接收数据,则接收数据(赋值给x),并执行该case
// ...
case ch3 <- y: // 如果ch3可以发送数据,则将y发送给信道ch3
// ...
default: // 如果所有信道都无法通信,则执行default
// ...
}
image.Decode
,因此可以区分不同包中的相同名称,例如:image.Decode
,utf16.Decode
cynhard.com/mytools/logs
,那么包的名称为logs
。包的导入路径应该是全球唯一的。为了避免冲突,应当以域名为路径的开头:
import(
"cynhard.com/tools/log"
"golang.org/x/net/html"
)
Go源文件中在包声明之后可以有一个或多个导入声明。每个导入声明可以只导入一个包,也可以导入用括号括起的包列表。通常使用包列表导入包。
import "fmt"
import "os"
import (
"fmt"
"os"
)
如果要导入的两个包名相同,则其中一个或两个都必须制定一个别名。这叫做重命名导入(renaming import):
import (
"crypto/rand"
mrand "math/rand" // 指定别名
)
包不能循环导入。发生循环导入时go的构建工具会报错。
如果一定要导入某个包使用它的副作用(例如导入时对包的初始化),则必须指定别名为空标识符(_
),这种导入称为空导入。
import _ "image/png" // 注册PNG解码器
包初始化从初始化包级别的变量开始,按照变量的生命顺序进行。但若是变量之间有依赖,则根据依赖求值顺序执行:
var a = b // a 在b初始化之后初始化
var b = 1 // b 最先初始化
GOPATH
以让Go工具集能够找到该目录。比如在Windows下配置环境变量只需要在命令行中执行set GOPATH=g:\projects\go
就可以了(g:\projects\go是我在自己电脑上的目录,需要根据实际情况换成自己的)。其他操作系统类似。工作空间包含三个子目录:src、bin、pkg。
以下是一个例子(本人的工作空间,Windows操作系统):
GOPATH\
bin\
helloworld.exe
pkg\
windows_amd64\
cynhard.com\
tools\
log.a
src\
cynhard.com\
tools\
log\
log.go
tests\
helloworld\
helloworld.go
可以执行go env查看所有环境变量:
G:\projects\go>go env
set GOARCH=amd64
set GOOS=windows
set GOPATH=g:\projects\go
set GOROOT=C:\Go
...
go run 将源代码编译、链接成可执行程序,并执行该程序。go run的参数为所要编译执行的文件的名称。该命令可以执行绝对路径或者相对路径(相对于工作空间),以下命令等价:
go run %GOPATH%\src\cynhard.com\tests\helloworld\helloworld.go
go run src\cynhard.com\tests\helloworld\helloworld.go
cd %GOPATH%\src\cynhard.com\tests\helloworld
go run helloworld.go
go build 用来编译包。如果包是一个库,则编译后没有结果,只是在编译过成中检查编译错误。如果是main包,则编译并链接成为一个可执行文件,放在当前目录下(执行go build时所在的目录)。go build后面的参数可以是报的导入路径,也可以是以.或..开头的相对路径,但不能是绝对路径:
// 只要设置了环境变量GOPATH,则可以在任何目录下编译包
cd anywhere
go build cynhard.com/tools/log
// 指定相对路径
cd %GOPATH%
go build .\src\cynhard.com\tools\log
// 不能是绝对路径!
go build %GOPATH%\src\cynhard.com\tools\log # ERROR!
go list查询 可用的包。go list检查工作空间中是否有相应的包,如果有,则打印导入路径,如果没有则报错:
g:\projects\go>go list cynhard.com/tools/log
cynhard.com/tools/log
// 不存在则报错
g:\projects\go>go list cynhard.com/tools/md5
can't load package: package cynhard.com/tools/md5: cannot find package "cynhard.com/tools/md5"
C:\Go\src\cynhard.com\tools\md5 (from $GOROOT)
g:\projects\go\src\cynhard.com\tools\md5 (from $GOPATH)
可以用…通配任意字符串,如果没有任何其他字符,则…表示列出所有可用包(包括GOROOT)下的:
// 用通配符...匹配部分导入路径
g:\projects\go>go list cynhard.com/...
cynhard.com/tests/helloworld
cynhard.com/tools/log
// 用通配符...打印所有包的导入路径
g:\projects\go>go list ...
archive/tar
archive/zip
bufio
bytes
cmd/addr2line
cmd/asm
cynhard.com/tests/helloworld
cynhard.com/tools/log
...
go doc 查看指定实体的注释,实体可以是包、函数、变量等。
g:\projects\go>go doc time
package time // import "time"
Package time provides functionality for measuring and displaying time.
The calendrical calculations always assume a Gregorian calendar, with no
leap seconds.
...
g:\projects\go>go doc time.Second
const (
Nanosecond Duration = 1
Microsecond = 1000 * Nanosecond
Millisecond = 1000 * Microsecond
Second = 1000 * Millisecond
Minute = 60 * Second
Hour = 60 * Minute
)
Common durations. There is no definition for units of Day or larger to avoid
confusion across daylight savings time zone transitions.
To count the number of units in a Duration, divide:
...
可以用go get下载所需要的包,下载的库会存储在工作空间中:
go get github.com/golang/lint/golint
go test
命令来测试包。go test
工具对测试的文件和测试函数有一定的要求。_test.go
。后缀为_test.go
的文件在go build
时不参与编译,只用在执行go test
时候进行测试。*_test.go
文件中有三种函数:Test 函数、Benchmark 函数以及Example 函数。 测试文件中必须引入testing包,测试函数必须有如下签名:
func TestName(t *test.T) {
// ...
}
测试函数必须以Test开头,后面的Name是可选的,Name必须是大写的,例如:
func TestLog(t *testing.T) { /* ... */ }
func TestPrint(t *testing.T) { /* ... */ }
go doc testing.T
查看详细文档说明。这里仅介绍比较常用的: 例子:
新建一个包文件夹: %GOPATH%\src\cynhard.com\tools\math,在包中添加一个max.go文件,内容如下:
package math
func Max(nums ...int) int {
max := 0
for _, num := range nums {
if (num > max) {
max = num;
}
}
return max;
}
在包文件夹中再新建一个测试文件 max_test.go,内容如下:
package math
import "testing"
func TestMaxPositive(t *testing.T) {
if (Max(1, 2, 3) != 3) {
t.Error("Error: Max(1, 2, 3) != 3")
}
}
func TestMaxNegative(t *testing.T) {
if (Max(-1, -2, -3) != -1) {
t.Error("Error: Max(-1, -2, -3) != -1")
}
}
执行命令go test -v cynhard.com/tools/math,其中-v用来打印测试的函数名称,执行时长和测试结果。
go test -v cynhard.com/tools/math
=== RUN TestMaxPositive
--- PASS: TestMaxPositive (0.00s)
=== RUN TestMaxNegative
--- FAIL: TestMaxNegative (0.00s)
max_test.go:13: Error: Max(-1, -2, -3) != -1
FAIL
exit status 1
FAIL cynhard.com/tools/math 0.378s
可以看到在测试负数最大值的时候出错了,因为在Max函数中max的初值为0,比任何负数都大,因此得不到正确的结果。修改max.go如下:
package math
func Max(nums ...int) int {
max := nums[0]
for _, num := range nums[1:] {
if (num > max) {
max = num;
}
}
return max;
}
再次执行go test -v,成功:
go test -v cynhard.com/tools/math
=== RUN TestMaxPositive
--- PASS: TestMaxPositive (0.00s)
=== RUN TestMaxNegative
--- PASS: TestMaxNegative (0.00s)
PASS
ok cynhard.com/tools/math 0.340s
另外,还可以用go test -run=”pattern”,其中pattern是正则表达式,用该命令执行匹配正则表达式的测试函数:
go test -v -run=".*Negative" cynhard.com/tools/math
=== RUN TestMaxNegative
--- PASS: TestMaxNegative (0.00s)
PASS
ok cynhard.com/tools/math 0.310s
即便如此Max(nums ...int)
仍无法处理空参数的情况:Max()
,这种情况可以让Max(nums ...int)
返回两个值,第二个值表示是否成功。这已不在本例范围之内,不再讨论。
.
,则表示执行所有的benchmark函数。例子:
新建一个包文件夹: %GOPATH%\src\cynhard.com\tools\sort,在包中添加一个sort.go文件,实现插入排序和快速排序。内容如下:
package sort
func InsertionSort(a []int) {
for j := 1; j < len(a); j++ {
key := a[j]
i := j - 1
for i >= 0 && a[i] > key {
a[i+1] = a[i]
i--
}
a[i+1] = key
}
}
func QuickSort(a []int) {
doQuickSort(a, 0, len(a))
}
func doQuickSort(a []int, p int, r int) {
if p < r {
q := partition(a, p, r)
doQuickSort(a, p, q)
doQuickSort(a, q+1, r)
}
}
func partition(a []int, p int, r int) int {
x := a[r-1]
i := p - 1
for j := p; j < r-1; j++ {
if a[j] <= x {
i++
a[i], a[j] = a[j], a[i]
}
}
a[i+1], a[r-1] = a[r-1], a[i+1]
return i + 1
}
创建测试文件,分别测试10、100、1000、10000、100000个数的插入排序和快速排序:
package sort
import (
"math/rand"
"testing"
"time"
)
func TestInsertionSort(t *testing.T) {
a := stuff(10)
InsertionSort(a)
t.Log(a)
}
func TestQuickSort(t *testing.T) {
a := stuff(10)
QuickSort(a)
t.Log(a)
}
func BenchmarkInsertionSort10(b *testing.B) {
benchmarkInsertionSort(b, 10)
}
func BenchmarkQuickSort10(b *testing.B) {
benchmarkQuickSort(b, 10)
}
func BenchmarkInsertionSort100(b *testing.B) {
benchmarkInsertionSort(b, 100)
}
func BenchmarkQuickSort100(b *testing.B) {
benchmarkQuickSort(b, 100)
}
func BenchmarkInsertionSort1000(b *testing.B) {
benchmarkInsertionSort(b, 1000)
}
func BenchmarkQuickSort1000(b *testing.B) {
benchmarkQuickSort(b, 1000)
}
func BenchmarkInsertionSort10000(b *testing.B) {
benchmarkInsertionSort(b, 10000)
}
func BenchmarkQuickSort10000(b *testing.B) {
benchmarkQuickSort(b, 10000)
}
func BenchmarkInsertionSort100000(b *testing.B) {
benchmarkInsertionSort(b, 100000)
}
func BenchmarkQuickSort100000(b *testing.B) {
benchmarkQuickSort(b, 100000)
}
func BenchmarkInsertionSort1000000(b *testing.B) {
benchmarkInsertionSort(b, 1000000)
}
func BenchmarkQuickSort1000000(b *testing.B) {
benchmarkQuickSort(b, 1000000)
}
func stuff(count int) (a []int) {
seed := time.Now().UTC().UnixNano()
rng := rand.New(rand.NewSource(seed))
a = make([]int, count, count)
for i := 0; i < count; i++ {
a[i] = rng.Intn(100000)
}
return
}
func benchmarkInsertionSort(b *testing.B, count int) {
for i := 0; i < b.N; i++ {
a := stuff(count)
InsertionSort(a)
}
}
func benchmarkQuickSort(b *testing.B, count int) {
for i := 0; i < b.N; i++ {
a := stuff(count)
QuickSort(a)
}
}
执行 go test -v -run=”” -bench=. cynhard.com/tools/sort,结果中的第一列是执行的函数,函数后面为GOMAXPROCS的值,在执行并发benchmark函数时起着重要作用。第二列是执行的次数。第三列是平均每次操作的用时。 结果如下,可见随着数量的增加,快速排序的增长速率更低。
go test -v -run="" -bench=. cynhard.com/tools/sort
=== RUN TestInsertionSort
--- PASS: TestInsertionSort (0.00s)
sort_test.go:12: [4549 6381 17871 20498 49962 53704 58549 66689 90263 97786]
=== RUN TestQuickSort
--- PASS: TestQuickSort (0.00s)
sort_test.go:18: [36071 40304 68543 71582 76753 76789 81815 87461 89632 90046]
BenchmarkInsertionSort10-4 200000 11128 ns/op
BenchmarkQuickSort10-4 200000 11197 ns/op
BenchmarkInsertionSort100-4 100000 17152 ns/op
BenchmarkQuickSort100-4 100000 15697 ns/op
BenchmarkInsertionSort1000-4 5000 375372 ns/op
BenchmarkQuickSort1000-4 20000 96516 ns/op
BenchmarkInsertionSort10000-4 50 33685936 ns/op
BenchmarkQuickSort10000-4 2000 987985 ns/op
BenchmarkInsertionSort100000-4 1 3324423000 ns/op
BenchmarkQuickSort100000-4 100 11273270 ns/op
BenchmarkInsertionSort1000000-4 1 344239744400 ns/op
BenchmarkQuickSort1000000-4 10 124190670 ns/op
PASS
ok cynhard.com/tools/sort 367.374s
Exmaple函数的第二个作用是验证输出结果。如果在函数的末尾写上// Output:注释的话,测试是会验证输出结果是否与注释的一致,如果不一致,则报错。
例如:
func ExampleInsertionSort() {
a := []int{3, 2, 4, 1, 5}
QuickSort(a)
fmt.Println(a)
// Output:
// [1 2 3 4 5]
}
func ExampleQuickSort() {
a := []int{3, 2, 4, 1, 5}
QuickSort(a)
fmt.Println(a)
// Output:
// [1 2 3 4]
}
执行go test -v -run="Example" cynhard.com/tools/sort
结果如下。可以看到在执行ExampleQuickSort()时出错了,把注释改成[1 2 3 4 5]
就可通过测试。
go test -v -run="Example" cynhard.com/tools/sort
=== RUN ExampleInsertionSort
--- PASS: ExampleInsertionSort (0.00s)
=== RUN ExampleQuickSort
--- FAIL: ExampleQuickSort (0.00s)
got:
[1 2 3 4 5]
want:
[1 2 3 4]
FAIL
exit status 1
FAIL cynhard.com/tools/sort 0.327s
reflect.Type
可以包含任意类型。reflect.TypeOf
接收一个interface{}
,返回这个接口的实际类型:
x := interface{}(3) // x类型为interface{}
t := reflect.TypeOf(x) // t 类型为int
reflect.Type
满足fmt.Stringer
。可以通过fmt.Println()
打印reflect.Type.String()
。也可以将reflect.Type
直接作为参数传递给fmt.Println()
打印,效果是一样的。
fmt.Println(t.String()) // 输出: int
fmt.Println(t) // 输出: int
当以%T
作为fmt.Printf
的打印参数时,fmt.Printf
在内部对参数调用reflect.TypeOf
,然后打印类型信息:
fmt.Printf("%T\n", 3) // 输出: int
reflect.Value
可以包含任何类型的值。reflect.ValueOf
接收一个interface{}
,返回一个reflect.Value
:
x := interface{}(3)
v := reflect.ValueOf(x) // v是一个reflect.Value
reflect.Value
满足fmt.Stringer
。可以通过fmt.Println()
打印reflect.Value.String()
。也可以将reflect.Value
直接作为参数传递给fmt.Println()
打印,效果是一样的。
fmt.Println(v.String()) // 打印:
fmt.Println(v) // 打印: 3
当以%v
作为fmt.Printf
的打印参数时,fmt.Printf
在内部对参数调用reflect.ValueOf
,然后打印值信息:
fmt.Printf("%v\n", v) // 打印: 3
通过调用Type()
方法返回一个Value
的类型:
t := v.Type() // t 是 reflect.Type
fmt.Println(t) // 打印: int
reflect.Value.Interface
方法返回一个interface{}
,仍保留reflect.Value的值:
v := reflect.ValueOf(3)
x := v.Interface() // 获取interface{}
fmt.Println(x.(int)) // 打印: 3
reflect.Value.Kind
获取值的类别:
type Point struct { X,Y int }
v := reflect.ValueOf(Point{1,2})
fmt.Println(v.Kind()) // 打印: struct
v = reflect.ValueOf([3]int{})
fmt.Println(v.Kind()) // 打印: array
reflect.Value.Kind()
方法返回reflect.Array
或者reflect.Slice
,则表示值为数组或切片。reflect.Value.Len()
方法获取数组或切片的长度reflect.Value.Cap()
方法获取数组或切片的容量reflect.Value.Index()
方法获取数组或切片的元素例子:
a := [...]int{1, 2, 3, 4, 5, 6}
v := reflect.ValueOf(a)
if v.Kind() == reflect.Array {
fmt.Println(v.Len()) // 打印: 6
fmt.Println(v.Cap()) // 打印: 6
fmt.Println(v.Index(3)) // 打印: 4
}
s := a[2:4]
v = reflect.ValueOf(s)
if v.Kind() == reflect.Slice {
fmt.Println(v.Len()) // 打印: 2
fmt.Println(v.Cap()) // 打印: 4
fmt.Println(v.Index(1)) // 打印:4
}
reflect.Value.Kind()
方法返回reflect.Struct
,则表示值为结构体。reflect.Value.NumField
返回结构体字段的个数。reflect.Value.Field(i)
返回第i个字段的reflect.Value
。reflect.Value.FieldByName(name)
返回名字为name
的字段。reflect.Type.Field(i)
返回第i个字段的reflect.Type
。可以通过reflect.Type.Name
获取字段的名字。例子:
type Point struct {X,Y int}
v := reflect.ValueOf(Point{1,2})
if v.Kind() == reflect.Struct {
fmt.Print("{")
sep := "";
for i := 0; i < v.NumField(); i++ {
fmt.Printf("%s%s:%v", sep, v.Type().Field(i).Name, v.Field(i))
sep = " ";
}
fmt.Println("}")
}
// 打印: {X:1 Y:2}
reflect.Value.Kind()
方法返回reflect.Map
,则表示值为Map。reflect.Value.MapKeys()
方法返回所有的Key切片。reflect.Value.MapIndex(key)
方法返回key所对应的的值。例子:
m := map[string]string{
"Name": "Cynhard",
"Country": "China",
}
v := reflect.ValueOf(m)
if v.Kind() == reflect.Map {
for _, key := range v.MapKeys() {
fmt.Printf("%s: %s\n", key, v.MapIndex(key))
}
}
// 打印:
// Name: Cynhard
// Country: China
reflect.Value.Kind()
方法返回reflect.Ptr
,则表示值为指针。reflect.Value.Elem()
方法返回指针指向的值,类型为reflect.Value
。reflect.Value.IsNil()
判断指针是否为空。例子:
x := 3
v := reflect.ValueOf(&x)
if v.Kind() == reflect.Ptr {
if !v.IsNil() {
fmt.Println(v.Elem()) // 打印3
}
}
reflect.Value.Kind()
方法返回reflect.Interface
,则表示值为接口。reflect.Value.IsNil()
判断接口是否为空。reflect.Value.Elem()
返回实际的值,类型为reflect.Value
。reflect.Value
包含可以取地址的值时,才可以对它赋值。reflect.Value.CanAddr()
判断一个值是否可以取地址。v := reflect.ValueOf(x)
获取的仅仅是x的值,并不是对x的封装。因此如果x是非引用类型,则v
并不可以取地址。如果x是引用类型,则可以通过v.Elem()
返回引用类型的解引用,该值是变量,因此可以取地址:
x := 3
fmt.Println(reflect.ValueOf(2).CanAddr()) // false
fmt.Println(reflect.ValueOf(x).CanAddr()) // false
a := reflect.ValueOf(&x)
fmt.Println(a.CanAddr()) // false
fmt.Println(a.Elem().CanAddr()) // true
reflect.Value.Addr()
获取值的地址。可以解引用这个地址,从而对变量赋值。
x := 3
a := reflect.ValueOf(&x).Elem()
*(a.Addr().Interface().(*int)) = 4
fmt.Println(x) // 打印: 4
reflect.Value.Set
可以给Value
赋值,赋值时类型必须匹配:
x := 3
a := reflect.ValueOf(&x).Elem()
a.Set(reflect.ValueOf(4))
fmt.Println(x) // 打印: 4
a.Set(reflect.ValueOf(5.0)) // 错误,类型不匹配
reflect.Value
提供了SetInt
、SetFloat
、SetString
等函数来赋值:
x := 3
a := reflect.ValueOf(&x).Elem()
a.SetInt(4)
fmt.Println(x) // 打印: 4
a.SetFloat(5.0) // 错误,类型不匹配
一个值可以取地址,但不一定可以被赋值。比如没有导出的结构体字段的值。reflect.Value.CanSet()
判断一个值是否可以取地址并被赋值:
stdout := reflect.ValueOf(os.Stdout).Elem()
fd := stdout.FieldByName("fd")
fmt.Println(fd.CanAddr(), fd.CanSet()) // true false
reflec.Value
调用方法reflect.Value.NumMethod()
返回方法的数量。reflect.Value.Method(i)
返回索引为i的方法的reflect.Value
值。reflect.Value.MethodByName(name)
返回名字为name的方法。reflect.Value.Call()
调用函数。参数和返回值都为[]reflect.Value
例子:
package main
import (
"fmt"
"reflect"
)
type Bird struct {
Name string
}
func (bird Bird) Fly(xFrom, yFrom int) (xTo, yTo int) {
xTo = xFrom + 5
yTo = yFrom + 5
fmt.Printf("%s fly from {%d, %d} to {%d, %d}\n",
bird.Name, xFrom, yFrom, xTo, yTo)
return
}
func (bird Bird) Sing() {
fmt.Println(bird.Name + " is singing");
}
func main() {
bird := Bird{
"Gold"}
v := reflect.ValueOf(bird)
fmt.Println(v.NumMethod()) // 打印: 2
for i := 0; i < v.NumMethod(); i++ {
fmt.Println(v.Method(i).Type())
}
// 打印:
// func(int, int) (int, int)
// func()
m := v.MethodByName("Fly")
p := []reflect.Value{reflect.ValueOf(1), reflect.ValueOf(2)}
r := m.Call(p) // 打印: Gold fly from {1, 2} to {6, 7}
fmt.Println(r[0], r[1]) // 打印: 6 7
}
reflec.Type
调用方法reflect.Type.NumMethod()
返回方法的个数。reflect.Type.Method(i)
返回索引为i的函数对象。该对象的类型为reflect.Method
reflect.Method.Name
为函数的名字。reflect.Method.Func
为reflect.Value
对象,该对象的类别为reflect.Func
。这个函数为方法表达式。reflect.Type.MethodByName(name)
获取名字为name的方法表达式。例子:
type Bird struct {
Name string
}
func (bird Bird) Fly(xFrom, yFrom int) (xTo, yTo int) {
xTo = xFrom + 5
yTo = yFrom + 5
fmt.Printf("%s fly from {%d, %d} to {%d, %d}\n",
bird.Name, xFrom, yFrom, xTo, yTo)
return
}
func (bird Bird) Sing() {
fmt.Println(bird.Name + " is singing");
}
func main() {
bird := Bird{
"Lily"}
t := reflect.TypeOf(bird)
for i := 0; i < t.NumMethod(); i++ {
fmt.Println(t.Method(i).Func.Type())
}
// 打印:
// func(main.Bird, int, int) (int, int)
// func(main.Bird)
m, _ := t.MethodByName("Sing")
p := []reflect.Value{reflect.ValueOf(bird)}
m.Func.Call(p) // 打印: Lily is singing
}
unsafe.Sizeof()
函数提供了类似C的sizeof()
的功能,获取操作数的位数。unsafe.SizeOf()
参数是可产生任何类型的表达式(表达式不参与求值),返回的是一个uintptr。unsafe.Sizeof()
同C中的sizeof()
一样,如果作用于指针,则仅返回指针所占字节数。例子:
import "unsafe"
x := 42
fmt.Println(unsafe.Sizeof(x)) // 8
fmt.Println(unsafe.Sizeof(&x)) // 8
fmt.Println(unsafe.Sizeof(
struct {
x int
y float32
}{})) // 16
unsafe.Alignof()
接收一个可产生任何类型的表达式(表达式不参与求值),返回使这个类型对齐时所需要的字节数。unsafe.OffsetOf
接收一个结构体字段选择器,返回该字段相对于结构体开始地址的偏移量。例子:
package main
import (
"fmt"
"unsafe"
)
func main() {
var x struct {
a bool
b int16
c []int
}
fmt.Printf("Sizeof(x)\t= %d\tAlignof(x)\t= %d\n",
unsafe.Sizeof(x), unsafe.Alignof(x))
fmt.Printf("Sizeof(x.a)\t= %d\tAlignof(x.a)\t= %d\tOffsetof(x.a)\t= %d\n",
unsafe.Sizeof(x.a), unsafe.Alignof(x.a), unsafe.Offsetof(x.a))
fmt.Printf("Sizeof(x.b)\t= %d\tAlignof(x.b)\t= %d\tOffsetof(x.b)\t= %d\n",
unsafe.Sizeof(x.b), unsafe.Alignof(x.b), unsafe.Offsetof(x.b))
fmt.Printf("Sizeof(x.c)\t= %d\tAlignof(x.c)\t= %d\tOffsetof(x.c)\t= %d\n",
unsafe.Sizeof(x.c), unsafe.Alignof(x.c), unsafe.Offsetof(x.c))
}
// Output:
// Sizeof(x) = 32 Alignof(x) = 8
// Sizeof(x.a) = 1 Alignof(x.a) = 1 Offsetof(x.a) = 0
// Sizeof(x.b) = 2 Alignof(x.b) = 2 Offsetof(x.b) = 2
// Sizeof(x.c) = 24 Alignof(x.c) = 8 Offsetof(x.c) = 8
unsafe.Pointer
相当于C中的指针。*T
可以转换为unsafe.Pointer
,unsafe.Pointer
可以转换为任何*T
。unsafe.Pointer
可直接操纵内存。unsafe.Pointer
可以转换为uintptr
,保存指针的地址的值。uintptr
也可以转换为unsafe.Pointer
,将地址值转换为指针。
a := [3]int{1, 2, 3}
p := (*int)(unsafe.Pointer(
uintptr(unsafe.Pointer(&a)) + unsafe.Sizeof(int(0))))
*p = 4
fmt.Println(a) // [1 4 3]
不要使用临时的uintptr变量保存指针地址。垃圾回收机制有可能移动变量,导致变量地址的改变,使uintptr
中保存的地址值失效。
a := [3]int{1, 2, 3}
tmp := uintptr(unsafe.Pointer(&a))
+ unsafe.Sizeof(int(0)) // Error! 使用uintptr保存地址值
p := (*int)(unsafe.Pointer(tmp)) // p 可能指向无效内存
*p = 4 // 可能改变无效内存的值
不要将引用计数为0的地址值转换为unitptr,因为垃圾回收机制可能将该地址分配的内存回收。
p := uintptr(unsafe.Pointer(new(int))) // wrong
// Error! 至此已经没有指针指向新开辟的内存,引用计数为0,
// 垃圾回收器可能已将该内存回收,因此p中保存的地址值可能已无效。
// 该赋值可能改变无效内存中的数据。
*(*int)(unsafe.Pointer(p)) = 4
例子:
新建一个C文件命名为hello.c,内容如下:
#include
void greet()
{
printf("Hello, I am a C function.\n");
}
void say(char *str)
{
printf("%s\n", str);
}
新建一个go文件,命名为callhello.go,内容如下:
package main
// #include "hello.c"
import "C"
import "unsafe"
func main() {
C.greet()
data := []byte("Call C function in go.")
C.say((*C.char)(unsafe.Pointer(&data[0])))
}
执行 go run callhello.go,结果如下:
Hello, I am a C function.
Call C function in go.
说明
import "C"
比较特殊,go没有名字叫做"C"
的包,这是一条特殊的预处理指令,在go build
编译器编译go源代码之前,会首先调用cgo处理import "C"
之前的注释里的内容。注意在注释和import "C"
之间没有空格。注释中可以包含#cgo指令,以提供C编译工具选项。例如下例中通过#cgo指令指示了C编译工具用来查找头文件的路径,以及C链接工具在链接时需要导入的库。
// #cgo CFLAGS: -I/usr/include
// #cgo LDFLAGS: -L/usr/lib -lbz2
// ...
import "C"
例子:
创建hello.h,代码如下:
#ifndef HELLO_H
#define HELLO_H
void Greet();
void say(char *str);
#endif
创建hello.c,代码如下:
#include
void say(char *str)
{
Greet();
printf("%s\n", str);
}
创建hello.go,代码如下。注意这里用了一条#cgo指令,目的是为了找到hello.c编译的静态库。
package main
//#cgo LDFLAGS:-L. -lhello
//#include "hello.h"
import "C"
import (
"fmt"
"unsafe"
)
//export Greet
func Greet() {
fmt.Println("I am go function")
}
func main() {
data := []byte("Call C function in go.")
C.say((*C.char)(unsafe.Pointer(&data[0])))
}
编译C静态库:
gcc -c hello.c
ar -r libhello.a hello.o
执行Go程序,得到运行结果:
go run hello.go
I am go function
Call C function in go.