Go学习笔记

简介

Go语言

  • Go是一种开放源码的程序设计语言,它意在使得人们能够方便地构建简单、可靠、高效的软件。
  • Go有时被称为“类C语言”或“21世纪的C”。从C中,Go集成了表达式语法、控制流语句、基本数据类型、按值调用的形参传递、指针,但比这些更重要的是,继承了C所强调的程序要编译成高效的机器码,并自然地与所处的操作系统提供的抽象机制相配合。

程序结构

  • 一个Go程序保存在一个或多个以后缀为 .go 的文件当中。每个文件在最开头包含包(package)声明,来指定该文件属于哪个包。包声明后面的是导入(import)声明,用来导入程序中用到的库。导入声明后面是包级别(package-level)的声明,包括类型、变量、常量以及函数的声明,包级别的声明在相同文件和相同包中的其他文件都可见。在函数中声明的变量为局部变量,只在函数中可见。
  • main函数是程序的入口。
  • 例子:

    package main  // 包声明
    
    import "fmt"  // 导入声明
    
    const pi = 3.14  // 包级别的声明
    
    func main() {  // main 同样是包级别的声明
        var f = 123.456  // 局部声明
        fmt.Printf("f = %g\n", f)
    }

变量和常量

标识符

  • Go语言中的命名和其他语言无异:字母、数字、下划线的组合,但第一个字符不能是数字。
  • Go语言中的名字区分大小写。
  • 不能用关键字作为名字。
  • 命名长度没有限制,但在Go的传统里越短越好。可见性越广,定义越长的名字就越不容易起冲突。
  • Go习惯用驼峰式命名。例如:parseRequestLine
  • 如果名称中包含首字母缩略词,则在命名时不用驼峰式命名。例如:HTMLEscapeescapeHTML而不是escapeHtml

关键字

Go有25个关键字:

breakdefaultfuncinterfaceselect
casedefergomapstruct
chanelsegotopackageswitch
constfallthroughifrangetype
continueforimportreturnvar

变量

变量声明

  • 用var声明定义变量以及它的类型和初值:var name type = expression
    • 类型或者= expression部分可以省略,但不能两者同时省略。
    • 如果类型省略,则编译器根据expression推断类型。
    • 如果省略= expression,则该变量初始为0值:
      - 数字初始为0
      - 布尔值初始为false
      - 字符串初始为空
      - 接口和引用初始为nil
      - 数组和结构体将其中的元素或字段初始为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
  • 指针的0值为nil
  • 可以返回函数内的局部变量的地址

    var p = f()
    func f() *int {
        v := 1
        return &v  // ok
    }

new函数

  • 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
    }()

变量的可见性

  • 如果一个变量定义在函数里,那么它是局部的。
  • 如果一个变量定义在所有函数外,则它在包里的所有文件中都可见。并且,如果名称首字母是大写,则在包外亦可见。(包的名字总是小写)

变量的生命期

  • 包变量(全局变量)的生命期与程序的生命期一致。局部变量的生命期从生命开始到该变量不可见为止。
  • Go语言有垃圾回收机制,因此不必担心内存的开辟和释放问题。
  • 变量是在堆上还是在栈上创建取决于该变量的生命期。
    • 若函数返回局部变量的地址,则该变量被分配在堆上。
    • 若变量仅在局部(函数体内,循环体内,if语句内等等)使用,则该变量分配在栈上,即使是用new()函数创建的变量也是如此。
    • 函数是被创建在栈上还是堆上由编译器决定,无需程序员干预。

词块

  • 作用域表示一个名字的作用范围。
  • 词块(lexical block)表示一个作用范围,该范围内限定了名字的作用域。以下范围都称为词块:整个源代码(又称为全局块(universe block))、包、文件、函数、for语句内、if语句内、switch语句内、case语句内,每一对大括号内。
  • 在不同词块内可声明同一个名字,内部词块中的名字会隐藏外部词块中的名字,但这是一种不好的风格。

常量

常量的声明

  • 用const声明常量: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 组成的表达式给第一个常量赋值,第一个常量之后的每一个常量都应用这个表达式,但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


2n112n11
int1616
int3232
rune32rune是int32的别名,
用来表示Unicode码点(code point)。
int6464
uint8


无符号
8


02n1
uint1616
uint3232
uint6464
int有符号

不同的编译器根据不同的平台设置不同的大小,
以达到最高效的利用硬件。
uint无符号
uintptr无符号uintptr是一个无符号整型,
仅用来兼容低级语言(比如C)。
  • 整型除了可以用10进制表示外,还可以用8进制和16进制表示。8进制前加0,16进制前加0x或者0X。

操作符

  • 可用于整型的操作符和优先级如下(优先级从高到低,每行优先级相同):

    */%<<>>&&^
    + |^
    ==!=<<=>>=
    &&
    ||

    前两行的操作符都有一个对应的复合赋值操作符,比如+对应+=

  • 算术运算符+ - * /可用于整型、浮点型、复数。
  • 取模运算符(%)仅可用于两个整型。如果取模时有负数,则最后的符号由被除数决定,比如:-5%3 与 -5%-3 结果都为 -2
  • 算术运算符的结果可能超出操作数的类型的范围。这就引发了溢出。运算时应注意选用合适的类型做运算,以免溢出。
  • 比较运算符 == != < <= > >=可应用于两个类型相同的基基本类型(整型、浮点型、字符串),结果为bool类型。
  • + - 亦可作为一元运算符,表示正负。
  • Go提供了如下的位运算符:

    &按位与
    |按位或
    ^异或
    &^与非
    <<左位移
    >>右位移
  • ^亦可作为一元运算符,表示按位取反。
  • &^表示与非,例如:z = x &^ y,如果y的某一位为1,则z的该位为0,否则z的该位和x的该位一致。
  • << 与 >>的右操作数必须为无符号数。>>的左操作数若为有符号数,则向右移的时候用符号位填补空位。

打印整型

  • 在使用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 '国'

浮点数

浮点数类型

类型精度最大值最小值
float326位math.MaxFloat32math.MinFloat32
float6415位math.MaxFloat64math.MinFloat64

打印浮点数

  • 用%f打印浮点数

特殊值:

特殊值意义例子
+Inf正无穷5.0/0.0
-Inf负无穷-5.0/0.0
NaNNot a Number0/0
  • math.IsNaN 测试一个值是否为NaN
  • math.NaN 返回NaN
  • NaN与NaN比较为false

复数

  • Go语言内置复数类型。有两种复数类型:complex64 complex128,complex64的实部和虚部位float32,complex128的实部和虚部是float64。
  • 用complex()创建一个复数类型,real()和imag()返回实部和虚部。虚部的字面值以数字后面加i表示。
  • Go支持复数的四则运算。比较操作符支持:==!=

布尔值

  • Go语言的bool值仅有true和false。
  • 以下操作产生bool值: 比较操作符:> >= < <= == !=),逻辑非:!
  • 两个产生bool值的表达式可以用逻辑与(&&)和逻辑或(||)连在一起,并产生短路行为(貌似所有的语言都是如此,至少我所知的是如此)。
  • 不能将数值或指针类型隐式转换成布尔值,反之亦然。

字符串

  • 字符串是一串byte序列。用string表示。
  • 字符串通常以UTF8编码存储Unicode码点(rune)。
  • len()函数返回字符串中byte的个数,非Unicode字符的个数。假设s是字符串,则下标操作符s[i]返回字符串中第i个byte的值,非第i个Unicode字符。
  • Go支持字符串切片:s[i:j]返回从第i个byte开始到第j个byte结束(不包含第j个byte)的字符串。切片时可以省略i和j当中的任意一个或全部。省略i则从0开始,省略j则到len(s)结束,都省略则表示从0到len(s)的字符串(即本身)。
  • string支持+操作符以拼接字符串。支持+=以拼接新的字符串并赋值给变量自身。
  • string支持比较操作符。以byte为单位逐一比较。
  • 字符串是不能改变的。例如 s[0]='v'得到编译错误。正是因为字符串不能改变,切片所返回的字符串可以和原字符串共用同一段内存,以提高效率。

字符串字面值

  • 双引号(”)或反引号( `)括起的byte序列为字符串字面值。
    • 双引号字符串中转义以\开始的字符
    • 反引号字符串中不转义以\开始的字符
  • 转义字符可以用16进制(以\x开头,后面接两个16进制字符,比如\xAB)或者8进制(以\开头,比如\377)表示。
  • 常用的转义字符:

    字符意义
    \n换行
    \r回车
    \t制表符
    \’单引号
    \”双引号
    \\反斜线
  • 反引号括起的字符串叫做原始字符串(raw string),其中以\开始的字符不转义。原始字符串可以跨越多行,但是Go会自动去掉其中的回车(\r),以使原始字符串在Linux和Windows下保持一致。原始字符串可以用在正则表达式中以避免过多的转义。
  • 因为Go程序源码总是保存为UTF8,而字符串也通常也以UTF8解析。所以可以在字符串字面值中写任何Unicode码点。

UTF-8

  • UTF-8以变长的方式编码Unicode。
  • Go的源代码以UTF-8方式存储。源码中的字符串也是以UTF-8方式处理。
  • Unicode转义:16位:\uhhhh;32位:\Uhhhhhhhh
  • 只有小于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: '好'
  • Go的解码器在解码UTF-8时,如果遇到非法字符序列,则转换为\uFFFD,这个字符一般是一个多边形的黑块中间有个?的字符:�
  • rune与UTF-8字符串互转:
    • string转为[]rune会自动解码,得到相应的rune序列。
    • []rune转为string会自动编码,得到相应的UTF-8字符串。
    • 数字转string会将数字先转为rune,然后对该rune进行编码生成相应的字符串。

复合类型

数组

  • 数组是一个或多个同类型元素组成的固定长度的序列。表示为[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]( 0ijcap(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

  • Go中map表示为map[K]V,K为键,V为值。K必须是可以用==比较的类型。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)
    }
  • 获取元素个数用内置函数len()。
  • 不能取元素地址:

    _ = &n["b"] // error
  • map的0值为nil。
  • 判断某个键是否在map中:

    age, ok := ages["bob"]  // 如果不存在键"bob",则ok为false 
  • 两个map不能比较,map只能和nil比较。

类型声明

  • 类型声明定义了一个新的类型,它以某个已经存在的类型作为其基础类型(underlying type),类型声明的语法为: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
    }
  • 一个结构体不能继承另一个结构体,Go中没有继承的概念和语法。只能用组合来实现重用。

结构体字面值

  • 结构体字面值为结构体类型后面接大括号,按顺序写字段的值,用逗号分隔。

    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操作JSON请见:Go操作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
    

注释

  • Go语言中单行注释用 // ,多行注释用 /* */。与 C/C++ 一样。

分支语句

if语句

  • if语句不需要将条件用小括号括起来,但是必须要用花括号把if里面的语句括起来。左花括号必须跟在if条件之后。
  • 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语句

  • 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")
    }

循环语句

  • Go仅支持一种循环语句:for。for语句有多种形式:
  • 第一种形式和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)
    }

break与continue

  • break跳过最内层循环。继续执行循环后面的下一条语句。
  • continue结束本次循环,并继续下一次循环。
  • 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

goto语句

  • goto语句通常用在机器生成的代码中,程序员用不到。

函数

函数声明

  • 基本形式:

    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 {
    }
  • 函数的签名由参数列表中的类型和顺序以及结果列表中的类型和顺序构成。函数的类型由签名决定。

  • Go不支持函数重载,不支持默认参数值,不支持参数命名调用。

参数传递

  • Go总是以传值的方式传递参数。

函数值

  • 函数可以作为值来使用。可以赋值给变量或者从函数返回。

    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陷阱之循环变量

  • 在使用匿名函数时,须特别注意在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陷阱之deferfor陷阱之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。defer修饰的语句将在函数执行结束时自动调用。
  • 延迟调用经常用在成对出现的操作。比如打开,关闭文件;连接,断开数据库;互斥量加锁,解锁等。
  • defer语句应尽可能的紧跟在开辟资源的语句后面。

    f, err := os.Open(filename)
    if err != nil {
        return nil, err
    }
    defer f.Close()  // 延迟调用Close()紧跟在Open()之后
  • Go中没有RAII,因此必须额外注意申请的资源必须显示释放。申请完资源马上以defer释放资源是一个好的习惯。

for陷阱之defer

  • 不要在循环体里用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

Panic

  • 运行时如果出现数组访问越界、解引用空指针等异常情况,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
  • panic机制和其他语言的异常有些相似,但是他们应用的时机不同。panic用在程序出现严重错误(通常不可恢复)的时候。

Recover

  • 内置函数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

使用fmt包进行输入输出

  • 用fmt.Scanln从控制台读取数据。用fmt.Println和fmt.Printf打印数据到控制台。
  • fmt.Printf类似于C中的printf,用来打印格式化的数据。第一个参数是用来格式化的字符串,该字符串用来指定之后的每一个参数的格式化形式。每一个被格式化的参数都被指定一个带有百分号的转换字符,转换字符的意义如下表:
字符意义
%d十进制整数
%x, %o, %b16进制、8进制、2进制整数
%f, %g, %e浮点数
%t布尔值: true false
%crune (Unicode码点)
%s字符串
%q双引号括起来的字符串,或者单引号括起来的rune值
%vreflect.Value
%Treflect.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
    }
    • 因为Go函数是值传递,如果拷贝代价太大,则应该使用指针作为接收者
    • 在需要修改对象成员的时候,必须以指针作为接收者
  • 表达式obj.Method称为选择器(selector),编译器在解析选择器时,首先查找该obj有没有Method方法,如果有,则调用,如果没有,则查找obj的字段有没有Method方法,如果有,则调用,如果没有则查找下一个字段,以此类推。
  • 不存在匿名方法。
  • 方法不能声明在结构体内。Go中没有方法重载,虚方法,构造函数,析构函数等概念。

方法调用

  • 方法调用:选择器后面加()

    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()
    }

Type Switch

  • 一个接口值可以存储多种具现类型值。比如一个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)。

    • 该switch语句用x的类型和每一个case后面的类型按顺序进行比较,如果满足则执行相应的case,如果都不匹配则执行default。执行后便结束switch,继续执行switch后面的语句。
    • case里不允许fallthrough语句。
  • 有时候我们需要在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: // ...
        }
    }

协程和信道

协程

  • 在Go中,一个活动的执行过程称为一个协程(goroutine),类似于线程的概念(但不等同)。
  • main()函数开始时创建的goroutine为主goroutine,当主goroutine结束时,其他goroutine强制结束。
  • 要创建一个goroutine,只需在语句前面加go即可。

    func main() {
        go func() { // 创建一个goroutine
            fmt.Println("Hello, goroutine!")
        }()
        // 很可能无法输出"Hello, goroutine!",主goroutine结束会强制结束其它goroutines
    }   

信道

  • 信道连接多个goroutines。信道是两个goroutines之间的通信机制。通过信道传递的数据类型称作信道的元素类型。传递类型T元素的信道的类型为chan T。
  • 用内置函数make()创建一个信道,第二个参数指定信道的容量。若不指定第二个参数或第二个参数为0,则为无缓冲信道;若第二个参数大于0则为有缓冲信道,参数的值即为缓冲区大小。

    ch := make(chan int)  // ch 类型为 'chan int',无缓冲信道
    ch := make(chan int, 0)  // ch为无缓冲信道
    ch := make(chan int, 3)  // ch位有缓冲信道,缓冲区大小为3
  • 信道是引用类型。
  • 两个信道可以用==比较,如果两个信道指向同一个数据结构,则两个信道相等。信道可以与nil比较。
  • 信道有两个主要操作:发送和接收。这两个操作都用<-表示。<-在信道的右侧表示发送,<-在信道的左侧表示接收:

    ch <- x  // send 
    x = <-ch  // receive and assign
    <-ch  // only receive
  • 通过内置函数close()关闭信道:

    close(ch)
    • 信道关闭后,不能再发送数据。
    • 信道关闭后,可以接收到已经发送的数据,接收完所有发送的数据后,再接收则得到信道元素类型的0值。
  • range for可访问信道中的数据。

无缓冲信道

  • 在一个无缓冲区信道中执行发送操作时,发送操作阻塞,直到另一个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) {  // 只接收
    }

有缓冲信道

  • 有缓冲信道有一个元素队列。队列的长度由创建时指定。
  • 在发送时,如果队列不满,则可以发送数据而不阻塞,如果队列满了,则发送操作阻塞,直到有数据被接收goroutine接收。在接收时,如果队列中有数据,则提取数据,如果没有数据,则阻塞,直到有数据从发送goroutine发送。
  • 内置函数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

for陷阱之go

  • 考虑以下用协程打印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   
        // ...
    }

协程与线程

  • 协程类似于线程,但不等同于线程。个人理解:如果说线程是系统层面的概念,协程就是Go语言层面的概念。但协程内部必然仍以线程实现,所以可以说协程是对线程的封装。
  • 操作系统为线程分配的栈空间是固定的(通常为2M),有时太多,有时却太少。而一个goroutine的栈空间是不固定的,开始时只是很小的内存,通常2KB。goroutine可以根据需要增加或缩减栈空间的大小。goroutine栈空间最大可以达到1GB(绝对go了)。
  • 线程需要通过切换上下文完成调度。而在Go中,Go runtime提供了自己的调度器,该调度器使用m:n轮流调度(m:n scheduling)方式处理goroutines的调度。Go的调度器将m个goroutines分配到n个系统线程上。和系统调度器不同,Go的调度器不是通过硬件定时器中断,而仅仅是语言层面上的中断。比如一个goroutine调用了time.Sleep或者在信道上阻塞时,调度器会让这个goroutine休眠并运行其他goroutine直到它可以被唤醒为止。由于不需要切换内核上下文,恢复goroutine比恢复线程消耗更少时间。
  • 环境变量GOMAXPROCS指定了最多同时有多少个活动的系统线程可以用于执行一个Go程序。默认值是CPU的数量。GOMAXPROCS就是m:n调度中的n。
  • Goroutine没有唯一标识。

简介

  • 任何包机制都是为设计和维护大型程序而产生的。包将一组相关的特性联系起来组成一个整体,和程序中的其他包相对独立。这种模块化设计让包可以共享给不同的工程重用。在机构范围内,或在全世界分发。
  • 包的作用是提供类似其他语言的库或者模块的概念。包可以单独编译。
  • 每一个包定义了一个唯一的名称空间。包中的每一个名字都与这个包相关联,不会与其他包中的名字冲突。
  • 包提供了封装。包可以将一些名字导出使其在包外可见,也可以将一些名字隐藏在包内用以包内部的实现。

包和文件

  • 包有一个或多个.go文件组成。通常这些文件在与包的导入路径同名的文件夹中,比如包名为:cynhard.com/mytools/logs,那么包的文件就存储在:G:/go/src/cynhard.com/mytools/logs文件夹中。
  • 包提供了名称空间,要引用包中的内容,则需要在包中实体名称之前,冠以包名和一个点,比如要引入image包中的Decode,则应该写作:image.Decode,因此可以区分不同包中的相同名称,例如:image.Decodeutf16.Decode
  • 在包级别中声明的名字,若为为大写,则包外可见(亦称作导出的),否则不可见。

包声明

  • 包声明必须是Go源代码中非注释的第一行。主要的目的是起一个默认的包名。其他包引入这个包中的名字是,需要指定这个报名。
  • 约定俗成,包的名字应该是导入路径(用/隔开的)的最后一段字符串。但是有三种例外情况。
    • 含有main函数的包名为必须为main
    • 后缀为_test.go的文件
    • 包名中带版本号的情况

包导入

导入路径

  • 导入路径就是在import声明中声明的字符串。导入路径唯一标识一个包。通常包的名字为导入路径的最后一个文件夹名,例如:如果包的导入路径为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的构建工具会报错。

空导入(Blank Imports)

  • 如果导入了包,却没有用其中的名字,则go构建工具会报错。
  • 如果一定要导入某个包使用它的副作用(例如导入时对包的初始化),则必须指定别名为空标识符(_),这种导入称为空导入。

    import _ "image/png"  // 注册PNG解码器

包初始化

  • 包初始化从初始化包级别的变量开始,按照变量的生命顺序进行。但若是变量之间有依赖,则根据依赖求值顺序执行:

    var a = b  // a 在b初始化之后初始化
    var b = 1  // b 最先初始化
  • 如果包中有多个文件,则按照编译顺序进行初始化。go一般按照文件名顺序进行编译。
  • 每个文件都可以包含一个或多个init()函数,在程序开始执行时,会自动调用所有的init()来进行额外的初始化工作。
  • 包的初始化顺序是按照导入的顺序决定的,先导入的包先初始化。
  • 在包级别中,每个声明的顺序无关紧要,先出现的声明可以引用后出现的声明。

GO工具集

工作空间(Workspace)

  • Go工作空间是组织源代码、库和可执行文件的目录。这个目录可以在电脑中的任意位置。Go工具集中的很多命令都是以相对路径为参数的,这个相对路径就是相对于工作空间目录而言的,因此在使用Go工具集时,需要配置环境变量GOPATH以让Go工具集能够找到该目录。比如在Windows下配置环境变量只需要在命令行中执行set GOPATH=g:\projects\go就可以了(g:\projects\go是我在自己电脑上的目录,需要根据实际情况换成自己的)。其他操作系统类似。
  • 工作空间包含三个子目录:src、bin、pkg。

    • src需要我们自己手动创建的目录,该目录是我们放源码的地方,在src下,包的源文件放在与包的导入路径一致的目录中,比如本人的一个包的导入路径为”cynhard.com/tools/log”,则把包中相应的文件放在%GOPATH%\src\cynhard.com\tools\log目录中。
    • 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
      • src中根据包的导入路径存储实现包的文件。
      • bin\helloworld.exe 是编译 cynhard.com/tests/helloworld 的结果。
      • pkg\windows_amd64\cynhard.com\tools\log.a 是编译 cynhard.com/tools/log 的结果。
  • 可以执行go env查看所有环境变量:

    G:\projects\go>go env
    set GOARCH=amd64
    set GOOS=windows
    set GOPATH=g:\projects\go
    set GOROOT=C:\Go
    ...

编译包

  • 最常使用到的工具就是Go编译工具集了,该工具集有三个常用的命令:go run,go build,go install
  • 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 install和go build相似,但是会在%GOPATH%\pkg下生成编译的库文件,在%GOPATH%\bin下生成可执行文件。

查询包

  • 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测试工具

  • 在Go中用go test命令来测试包。
  • go test工具对测试的文件和测试函数有一定的要求。
  • 测试的代码需要放在包所在的文件夹中,后缀必须为_test.go。后缀为_test.go的文件在go build时不参与编译,只用在执行go test时候进行测试。
  • *_test.go文件中有三种函数:Test 函数、Benchmark 函数以及Example 函数。
    • Test函数以Test开头,用来写测试用例,验证程序逻辑的正确性。
    • Benchmark函数以Benchmark开头,用来测试性能。
    • Example函数以Example开头,提供文档。

Test函数

  • 测试文件中必须引入testing包,测试函数必须有如下签名:

    func TestName(t *test.T) {
        // ...
    }
  • 测试函数必须以Test开头,后面的Name是可选的,Name必须是大写的,例如:

    func TestLog(t *testing.T) { /* ... */ }
    func TestPrint(t *testing.T) { /* ... */ }
  • *testing.T 类型中提供了报告错误,打印日志等方法。可以通过go doc testing.T查看详细文档说明。这里仅介绍比较常用的:
    • func (c *T) Error(args …interface{}) 用来打印日志。但并不结束测试函数。
    • func (c *T) Errorf(format string, args …interface{}),和Error功能一致,参数是以格式化打印的,类似于Printf。
    • func (c *T) Fatal(args …interface{}) 用来打印日志并立即结束测试函数。
    • func (c *T) Fatalf(format string, args …interface{}),和Fatal功能一致,参数是以格式化打印的,类似于Printf。
  • 例子:

    • 设置GOPATH为自己的工作目录。
    • 新建一个包文件夹: %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函数

  • Benchmark函数用来测试一个函数的性能。一个Benchmark函数类似于Test函数,不同的是以Benchmark开头,参数为*testing.B。*testing.B导出了一个字段N表示循环的次数,Go在执行benchmark函数时,首先为N指定一个合适的初始值,再根据需要增加N的值,重新执行benchmark函数。
  • 在执行benchmark函数时,必须指定-bench参数,后面接正则表达式,如果是.,则表示执行所有的benchmark函数。
  • 例子:

    • 设置GOPATH为自己的工作目录。
    • 新建一个包文件夹: %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

Example函数

  • Example函数没有参数,也没有返回值。
  • Example函数主要的作用是文档。godoc服务器将Exmaple函数嵌入被测试函数的文档里。
  • 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
  • Example函数也可以用在Go Playground里做实验,让用户可以有个快速的体验。

反射

简介

  • Go提供了反射机制,反射可以在运行时改变一个变量的状态,检查一个变量的值,调用变量的方法,应用针对变量内部形态的操作。反射让我们可以把类型本身当成一级变量。
  • reflact包提供了反射机制。其中有两个重要的类型:reflect.Type和reflect.Value,分别表示类型和值。

reflect.Type

  • 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.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()) // 打印: <int Value>
    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}

访问Map

  • 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提供了SetIntSetFloatSetString等函数来赋值:

    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.Funcreflect.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
    }

低级编程

简介

  • Go提供了很多安全机制,但有时候我们需要提高程序性能,或者与其他低级语言交互,或者实现一个Go无法实现的功能,这时候就需要一些工具来处理低级编程。Go的unsafe包提供了低级编程工具。

unsafe.Sizeof

  • Go的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

Alignof和Offsetof

  • 如果一个变量的内存地址为这个变量类型长度的整数倍,则称为对齐。计算机提取和存储对齐的变量会有更高的效率。因此编译器为了提高存取效率,会在复合数据类型中增加一定的空间,来使其中的元素对齐。
  • 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

  • unsafe.Pointer相当于C中的指针。
  • 任何*T可以转换为unsafe.Pointerunsafe.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

cgo

简介

  • cgo用于处理C代码和go代码的相互调用。
  • cgo这种工具叫做foreign-function interface(FFI)。

cgo的使用

  • 例子:

    • 新建一个C文件命名为hello.c,内容如下:

      
      #include <stdio.h>
      
      
      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文件中的函数或类型声明所对应的Go的声明。cgo调用C的编译器以一个特殊的方式来编译import之前的注释中的代码,用以确定需要在临时包中生成的对应的Go声明。
    • 注释中可以包含#cgo指令,以提供C编译工具选项。例如下例中通过#cgo指令指示了C编译工具用来查找头文件的路径,以及C链接工具在链接时需要导入的库。

      // #cgo CFLAGS: -I/usr/include
      // #cgo LDFLAGS: -L/usr/lib -lbz2
      // ...
      import "C"

C函数调用go函数

  • C函数调用go函数时,仅需在C中声明函数原型,在Go中实现就可以了。实际上应该叫做用Go语言实现C函数比较恰当。
  • 为了导出Go中的函数,需要在导出的函数之前加上注释//export FuncName,这样cgo才会将这个函数导出到临时包”C”供C函数使用。
  • 例子:

    • 创建hello.h,代码如下:

       #ifndef HELLO_H
       #define HELLO_H
      
       void Greet();
      
       void say(char *str);
      
       #endif
    • 创建hello.c,代码如下:

       #include <stdio.h>
       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.
  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值