Go语言的发现与理解

以下内容是我在学习和研究Go时,对Go的特性、重点和注意事项的提取、精练和总结,还有一些学习笔记(注:部分笔记是摘抄的原文重点语句);在本文中,不但详细严谨地罗列了Go各特性的具体规则,对于一些较难理解的特性也给出了其内部的运行机制,方便大家深入理解;

目录

内容

字面量语法总结:开始

类型语法

数组

语法:

[数组的长度]元素类型

示例:

[3]string

切片slice

语法:

[]元素类型

示例:

[]string

map

语法:

map[key的类型]value的类型

示例:

map[string]int

结构体struct

语法:

struct {
    成员名1 成员类型1
    成员名2 成员类型2
    成员名3 成员类型3
}

示例:

struct{ Name string, Age  int }

字面量语法

类型{成员列表}

字面量语法示例

数组字面量语法:

//定义个类型别名,通过类型别名来创建字面量实例
type GBY [3]string
a := GBY{"郭","斌","勇"}

//创建长度为3的数组,并用一组元素来初始化
a := [3]string{"郭","斌","勇"}

//创建长度为4的数组,并用一组元素("郭","斌","勇")来初始化,末指定初始值的元素默认为元素类型的零值
b := [4]string{"郭","斌","勇"}

//数组的长度自动设置为初始化元素的个数
c := [...]string{"郭","斌","勇"}


//创建长度为5的数组,并指定索引2、5、3的元素值,索引的顺序可任意放置,索引也可以用常量表示,末指定初始值的元素认为元素类型的零值
const index5  = 5
d := [6]string{2:"郭",index5:"斌",3:"勇"}

//数组的长度会自动根据最大的索引来计算
e := [...]string{4:"郭"}


//定义个类型别名,通过类型别名来创建字面量实例
type GBY [3]string
g := GBY{"郭","斌","勇"}

切片slice字面量语法:

//创建切片,并用一组元素来初始化
a := []string{"郭","斌","勇"}

//创建切片,切片的长度会自动根据最大的索引来计算,并指定索引2、5、3的元素值,索引的顺序可任意放置,索引也可以用常量表示,末指定初始值的元素认为元素类型的零值
const index5  = 5
b := []string{2:"郭",index5:"斌",3:"勇"}


//定义个类型别名,通过类型别名来创建字面量实例
type GBY []string
g := GBY{"郭","斌","勇"}

map字面量语法:

//创建一个空的map实例
a := map[string]int{}

//创建一个map实例,并且指定初始的key和value
b := map[string]int{"郭斌勇":28,"闯":18}


//定义个类型别名,通过类型别名来创建字面量实例
type GBY map[string]int
g := GBY{"郭斌勇":28,"闯":18}

结构体struct字面量语法:
以结构体成员定义的顺序为
每个结构体成员指定一个面值。

type Point struct{ X, Y int }
p := Point{1, 2}

以成员名字和相应的值来初始化,可以包含部分或全部的成员,如果成员被忽略的话将默认用零值。因为,提供了成员的名字,所有成员出现的顺序并不重要。

type Point struct{ X  int, Y int }
p := Point{Y:2,X:1}

//直接定义类型和实例

g := struct{ X, Y int }{"郭斌勇":28,"闯":18}

字面量语法总结:结束

常量

常量表达式的值在编译期计算,而不是在运行期。每种常量的潜在类型都是基础类型:bloolean、string 或 数字;

常量间的所有算术运算、逻辑运算 和 比较运算 的结果也是常量。
对常量的类型转换 或 以下函数调用都是返回常量结果:len、cap、real、imag、comples 和 unsafe.Sizeof

因为它们是值是在编译期就确定的,因此常量可以是构成类型的一部分,例如:用于指定数组类型的长度:

const gby  = 4
var p [gby]byte 

如果是批量声明的常量,除了第一个外,其它的常量右边的初始化表达式都可以省略,如果省略初始化表达式,则表示使用前面常量的初始化表达式 和 类型。

例如:

const (
    a = 1
    b
    c = 2
    d
)

fmt.Println(a,b,c,d) // "1 1 2 2"

字面常量的类型

所谓字面常量(literal),是指程序中硬编码的常量,如:

-12
3.14159265358979323846 // 浮点类型的常量 
3.2+12i // 复数类型的常量 
true // 布尔类型的常量 
"foo" // 字符串常量

在其他语言中,常量通常有特定的类型,比如-12在C语言中会认为是一个int类型的常量。 如果要指定一个值为-12的long类型常量,需要写成-12l,这有点违反人们的直观感觉。Go语言 的字面常量更接近我们自然语言中的常量概念,它是无类型的。只要这个常量在相应类型的值域 范围内,就可以作为该类型的常量,比如上面的常量-12,它可以赋值给int、uint、int32、 int64、float32、float64、complex64、complex128等类型的变量。

预定义常量

Go语言预定义了这些常量:true、false 和 iota ;

iota 比较特殊,可以被认为是一个可被编译器修改的常量,在每一个 const 关键字出现时被重置为 0 ,然后在下一个 const 出现之前,每出现一次 iota ,其所代表的数字会自动增加 1 ;

如果两个 const 的赋值语句的表达式是一样的,那么可以省略后一个赋值表达式。因此:

const (
    a = iota    //0
    b = iota    //1
    c = iota    //2
    d = iota    //3
)

可被简写为:

const (
    a = iota    //0
    b           //1
    c           //2
    d           //3
)

常量的类型

Go的常量定义可以限定常量类型,但不是必需的。如果定义常量时没有指定类型,那么它 与字面常量一样,是无类型常量。
验证代码:

const gby  = 34.0  //定义常量
var dyx = 34.0  //定义变量

var wxl int = dyx  //报错:Cannot use 'dyx' (type float64) as type int in assignment  
var gm int = gby  //正常

常量定义的右值也可以是一个在编译期运算的常量表达式,比如
const mask = 1 << 3 由于常量的赋值是一个编译期行为,所以右值不能出现任何需要运行期才能得出结果的表达式,比如试图以如下方式定义常量就会导致编译错误: const Home = os.GetEnv("HOME") 原因很简单,os.GetEnv()只有在运行期才能知道返回结果,在编译期并不能确定,所以无法作为常量定义的右值。

无类型常量

编译器会为没有明确基础类型的数字常量提供比基础类型有更高精度的算术运算;可以认为至少有256bit的运算精度。共有六种未明确类型的常量类型,分别是无类型的布尔型、无类型的整数、无类型的字符、无类型的浮点数、无类型的复数、无类的字符串。

通过延迟明确常量的具体类型,无类型的常量不仅可以提供更高的运算精度,而且可以直接用于更多的表达式而不需要显式的类型转换;

例如下面的代码:gby 和 dyx 的值已经超出 Go 语言中 任何整数类型能表达的范围,但是它们依然是合法的常量,而且下面的常量表达式 gby/dyx 依然有效, 因为它是在编译期计算出来的,并且结果常量是 输出:10240 ,是 Go 语言 int 变量能有效表示的;

const gby  = 1208925819614629174706176
const dyx  = 118059160717411303424

fmt.Println(gby/dyx)  //输出:10240

变量

变量声明的语法如下:

var 变量名字 类型 = 表达式

其中 类型= 表达式 两个部分可以省略其中一个。如果省略了类型信息,那么将根据初始化表达式来推导变量的类型信息。如果初始化表达式被省略,那么将用相应类型的零值初始化该变量。数值类型变量对应的零值是 0 ,布尔类型变量对应的零值是 false ,字符串对应的零值是空字符串,接口或引用类型(包括 slice、map、chan和函数)变量对应的零值是nil。数组或结构体等聚合类型对应的零值是每个元素或字段都是对应的零值;零值初始化机制可以确保每个声明的变量总是有一个良好定义的值,因此在Go语言中不存在未初始化的变量。

简短变量声明

简短变量的声明语法如下

变量名 := 表达工

注意:

  • 简短变量在声明时,不能指定变量的类型;
  • := 是一个变量声明语句,而 = 是一个变量赋值操作。同样,多个简短变量的声明 和 元组的多重赋值 也不是一回事,后者是将右边各个表达式值赋值给左边对应位置的各个变量;
  • 简短变量声明左边的变量可能并不是全部都是刚刚声明的。如果有一些已经在相同的词法域声明过了,那么简短变量声明语句对这些已经声明过变量就只有赋值行为了;
    在下面的代码中,第一个语句声明了 gbydyx 两个变量。 在第二个语句 只声明了 wxl 一个变量,然后对已经声明的 gby 进行了赋值操作;
    gby , dyx :=  10 , 20
    wxl , gby := 30 , 40
    
  • 简短变量声明语句中必须至少要声明一个新的变量;下面的代码将不能编译通过:
    gby , dyx :=  10 , 20
    gby , dyx :=  30 , 40
    
  • 简短变量声明语句只有对已经在同级词法域声明过的变量才和赋值操作等价,如果变量是在外部词法域声明的,那么简短变量声明语句将会在当前词法域重新声明一个新的变量;

指针

  • 任何类型的指针的零值都是 nil 。 如果 p != nil 值为真,那么就可以判定 p 所指向的变量是有效的;
  • 在Go语言中,返回函数中局部变量的地址也是安全的;
    例如下面的代码,调用 f 函数时创建局部变量v,在局部变量地址被返回之后依然有效,因为指针p依然引用这个变量,并且每次调用f函数都将返回不同的结果;
    var p = f()
    
    func f() *int  {
     v := 1
     return &v
    }
    

阻止语句结束

Go语言中不需要在语句后面加 ; ,编译器会自动换在行处自动识别并决定是否需要加入 ; ,如果需要让一条语句占用多行,可以在行尾处加上 , ,如下:

fmt.Println(a,
        b,  //最后插入的逗号不会导致编译器的错误,这是Go编译器的一个特性
        )

变量的逃逸

如果局部变量被其作用域外的指针引用了,则称该局部变量逃逸了;

生命周期

  • 在包一级声明的变量的生命周期和整个程序的运行周期是一致的;局部变量的声明周期是从创建该变量的声明语句开始,直到该变量不再被引用为止;

  • 局部变量的分配的位置(分配在栈上 或 堆上)是由Go语言的编译器决定的,并不由用 var 还是 new 的声明方式决定的;对于逃逸的局部变量会被分配到堆上,而对于非逃逸的局部变量可能会被分配在栈上,也可能会被分配在堆上;

  • 逃逸的变量需要额外分配内存,同时对性能的优化可能会产生细微的影响;

自增 和 自减

自增 ++ 和 自减 -- 是语句,而不是表达式;所以像 x = i++ 之类的表达式是错误的;

赋值 和 相等

  • 赋值语句左边的变量和右边最终的求到的值必须有相同的数据类型。
  • 对于任何类型的值的相等比较,第二个值必须对第一个值的类型的变量是可赋值的,才可以进行相等比较;

元组赋值

在元组赋值之前,会先对赋值语句右边的所有表达式进行求值,然后再统一更新左边对应变量的值;

类型转换

对于每一个类型 T ,都有一个对应的类型转换操作 T(x) ,用于将 x 转为 T 类型(如果 T 是指针类型,可能需要小括狐包着 T ,比如: (*int)(0)。只有当两个类型的底层基础类型相同 或者 两者都是指向相同底层结构的指针类型时,才以允许这种转型操作,这些转换只改变类型而不影响值本身。如果 x 是可以赋值给 T 类型的值,那么 x 就一定可以被转为 T 类型;

数值类型之间也是可以转换的,字符串 和 一些特定类型的 slice 之间也是可以转换的。不过,这些转换可能会改变值的表现。例如:将一个浮点数转为整数将会丢弃小数部分,将一个字符串转为 []byte 类型的 slice 将拷贝一个字符串数据的副本。在任何情况下,运行时不会发生转换失败的错误,错误只会发生在编译阶段;

底层数据类型决定了内部结构 和 表达方式, 也决定是否可以像底层类型一样对内置运算符是否支持。对于高层类型的底层数据类型支持的内置运算符,该高层类型也同样支持;

对于内置运算符,可以对 命名类型的变量 和 有相同类型的变量 或者 有相同底层类型的未命名类型的值 进行运算,但是如果两个值有着不同的命名类型,则不能直接进行运算;

示例:

func main() {

    type GBY int64
    type DYX int64

    const g GBY = 9
    const b GBY = 5

    var y = g - b

    fmt.Printf("类型:%T,值:%v",y,y)   //输出:类型:main.GBY,值:4

    const d DYX = 7
    var h = g - d  //报错:Invalid operation: g - d (mismatched types GBY and DYX)


}

在 main 函数之前执行的解决方案

在包级别声明的变量会在main入口函数执行前完成初始化,局部变量将在声明语句执行到的时候完成初始化;

验证代码

func main() {
    fmt.Println("--main")
}

var gby = hbj()

func hbj()int  {
    fmt.Println("--hbj")
    return 34
}

输出日志:

--hbj
--main

利用此特性,也可实现在main函数执行之前执行某些函数;

包会导出以大写字母开头的标识符;因为汉字没有大小写,因此汉字开头的名字是不被导出的;

包级别的标识符 在 该包的任何文件文件中都可以直接使用;

每个源文件的包声明前紧跟着的注释是包注释;通常,包注释的第一句是包的功能概要说明,但不是必须的。一个包通常只有一个源文件有包注释,如果有多个包注释,目前的文档工具会根据源文件名的先后顺序将它们链接为一个包注释。如果包注释很大,通常会被放到一个独立的 doc.go 文件中。

包的初始化

  • Go语言的构建工具首先会将 .go 文件根据文件名排序,然后依次调用编译器编译;
  • 如果包中有多个 .go 文件,它们将按照发给编译器的顺序进行初始化;
  • 编译器以导入声明的顺序初始化每个导入的包;
  • 每个包只会被初始化一次;
  • 每个包在初初始化时,会先初始化其依赖的包;
  • 包的初始化首先是解决包级变量的依赖顺序,然后按照包级变量声明出现的顺序依次初始化,main 函数最后被初始化;
  • 每个文件都可以包含多个 init 初始化函数,它们会在程序开始执行时(注意:此时包已经初始化完毕)且 main 函数执行前,按照声明的顺序被自动被调用;init 初始化函数除了不能被调用或引用外,其他行为和普通函数类似;

验证代码:

func init()  {
    fmt.Println("init函数-up")
}


//通过立即执行的函数输出内容
var up = func ()string{
    var str = "up"
    fmt.Println(str)
    return str
}()


func main() {

    fmt.Println("main函数")
}

//通过立即执行的函数输出内容
var down = func ()string{
    var str = "down"
    fmt.Println(str)
    return str
}()



func init()  {
    fmt.Println("init函数-down")
}

输出结果:

up
down
init函数-up
init函数-down
main函数

作用域

一个声明语句将程序中的实体和一个名字关联,比如一个函数或一个变量。声明语句的作用域是指源代码中可以有效使用这个名字的范围;

不要将作用霸和生命周期混为一谈。声明语句的作用域对应的是一个源代码的文本域;它是一个编译时的属性。一个变量的生命周期是指程序运行时变量存在的有效时间段,在此区域内它可以被程序的其他部分引用;是一个运行时的概念。

语法块是由花括弧所包含的一系列语句,就像 函数体 或 循环体 花括弧对应的语法块一样。语法块内部声明的名字是无法被外部语法块访问的。语法块决定了内部声明的名字的作用域范围。我们可以这样理解,语法块可以包含其他类似组批量声明等没有用花括弧包含的代码,我们称之为语法块。最大级别的语法块为整个源代码,称为全局语法块;然后是每个包的包语法块;然后是文件级的文件语法块;每个for、if和 switch语句的语法块;每个switch或select的分支也有独立的语法块;当然也包括显式书写的语法块(花括弧包含的语句)。

声明语句对应的语法域决定了作用域的大小。对于内置的类型、函数 和 常量,比如 int、len 和 true 等是在全局作用域的,因此可以在整个程序中直接使用。任何在函数外部(也就是包级语法域)声明的名字可以在同一个包的任何源文件中访问的。对于导入的包,则是对应的源文件级的作用域,因此只能在当前的源文件中访问导入的包,当前包的其它源文件无法访问在当前源文件导入的包。

控制流标号(就是break、continue或goto语句后面跟着的那种标号),则是函数级的作用域。

一个程序可能包含多个同名的声明,只要它们在不同的词法域就没有关系。当编译器遇到一个名字的引用时,它首先从最内层的词法域向全局的作用域查找。如果查找失败,则报告 未声明的名字 这样的错误。如果名字在内部和外部的块分别声明过,则内部块的声明先被找到。在这种情况下,内部声明屏蔽了外部同名的声明,让外部的声明的名字无法被访问;

并不是所有的词法域都显式地对应到由花括弧包含的语句;还有一些隐含的规则,比如 for 语句: for 语句创建了两个词法域,一个是for的循环体部分的显式的词法域,另一个是for循环的初始化语句、条件测试语句、迭代语句 和 循环体 所对应的词法域;

在包级别,声明的顺序并不影响作用域范围,因此一个先声明的可以引用它自身或者是引用后面的一个声明,这可以让我们定义一些相互嵌套或递归的类型或函数。但是如果一个变量或常量递归引用了自身,则会产生编译错误;

运算符

二元运算符有5种优先级。对于相同优先级的运算符,使用左优先结合规则;使用括号可以明确优先顺序;

取模运算符 % 仅用于整数的运算。在 Go 语言中, % 取模运算符的符号 和 被取模数的符号总是一致的,因此,-5%3-5%-3 的结果都是 -2

除法运算符 / 的行为则依赖于操作数是否全为整数;如果操作数全为整数,则结果也为整数,并且会向着 0 方向截断小数;如果操作数包含有小数,则结果为小数;

如果一个算述运算的结果,不管是有符号或者无符号的,如果需要更多的bit位才能正确表示的话,就说明计算结果是溢出了。超出的高位的bit位部分将会被丢弃。如果原始的数值是有符号类型,而且最左边的bit位是1的话,那么最终结果可能是负的;

事实上,布尔类型、数字类型 和 字符串 等基本类型都是可比较的,也就是说两个相同类型的值可以用 == 和 != 进行比较。此外,整数、浮点数 和 字符串 可以根据比较结果排序。

对于整数, +x0+x 的简写, -x 则是 0-x 的简写;
对于浮点数 和 复数, +x 就是 x-x 则是 x 的负数;

Go 语言提供了以下几个 bit 位操作运算符,其中前4个操作运算符并不区分是有符号还是无符号数;

&       位运算 AND
|       位运算 OR
^       位运算 XOR
&^      位清空 (AND NOT)
<<      左移
>>      右移

左移运算用零值填充右边空缺的bit位,无符号数的右移运算也是用0填充左边空缺的bit位,但是有符号数的右移运算会用符号位的值填充左边空缺的bit位。因为这个原因,最好用无符号运算,这样你可以将整数完全当作一个bit位模式处理;

尽管Go语言提供了无符号数和运算,即使数值本身不可能出现负数我们还是倾向于使用有符号的int类型,就像数组的长度那样,虽然使用uint无符号类型似乎是一个更合理的选择。事实上,内置的len函数返回一个有符号的int,我们可以像下面例子那样处理逆循环:

medals := []string{"goid","silver","bronze"}

for i := len(medals) - 1;i >= 0 ; i-- {
fmt.Println(medals[i])
}

如果 len 函数返回一个无符号数,那么 i 也将是无符号的 uint 类型,然后条件 i >= 0 则永远为真。在三次抚今追昔之后,也就是 i == 0 时, i-- 语句并不会返回 -1 ,而是变成一个 uint 类型的最大值,然后 medals[i] 表达式将发生运行时异常,也就是访问一个slice范围以外的元素;

出于这个原因,无符号数往往只有在位运算或其它特殊的运算场景才会使用,就像bit集合、分析二进行文件格式或者是哈希和加密操作等。它们通常并不用于仅仅是表达非负数量的场合。

浮点数到整数的转换将丢失小数部分,然后向数轴上零方向截断。

逻辑运算符

  • &&|| 是短路操作符;

字符串

字符串是不可改变的字节序列;字符串可以包含任意的数据,包括 byte 值 0 ;文本字符串通常被解释为采用 UTF8 编码的 Unicode 码点 (rune) 序列;

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

不变性意味如果两个字符串共享相同的底层数据的话也是安全的,这便利复制任何长度的字符串代价是低廉的。同样,一个字符串 s 和 对应的子字符串切片 s[7:] 的操作也可以安全地共享了相同的内存,因此字符串切片操作代价也是低廉的。在这两种情况下都没有必要分配新的内存。

数组

数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成;
默认情况下,数组的每个元素都被初始化为元素类型对应的零值;
数组的长度是数组类型的一个组成部分,因此 [3]int[4]int 是两种不同的数组类型。
数组的长度必须是常量表达式,因为数组的长度需要在编译阶段确定;

数组在初始化时,也可以指定一个索引和对应的值列表,如下:

const a = 2
var hbj = [...]string{a:"gby",5:"dyx"}

在这种形式的数组字面值形式中,初始化索引的顺序是无关紧要的,而且没用到的索引可以省略,未设置初始值的元素将用零值初始化;

如果一个数组的元素类型是可以相互比较的,那么数组类型也是可以相互比较的,这时候我们可以直接通过 == 比较运算符来比较两个数组,只有当两个数组的所有元素都相等的时候数组才是相等的,反之,则不相等;

数组的类型

语法:

[数组的长度]元素类型

数组的长度是数组类型的一个组成部分,因此 [3]int[4]int 是两种不同的数组类型。

数组字面量语法

//创建长度为3的数组,并用一组元素来初始化
a := [3]string{"郭","斌","勇"}

//创建长度为4的数组,并用一组元素("郭","斌","勇")来初始化,末指定初始值的元素默认为元素类型的零值
b := [4]string{"郭","斌","勇"}

//数组的长度自动设置为初始化元素的个数
c := [...]string{"郭","斌","勇"}


//创建长度为5的数组,并指定索引2、5、3的元素值,索引的顺序可任意放置,索引也可以用常量表示,末指定初始值的元素认为元素类型的零值
const index5  = 5
d := [6]string{2:"郭",index5:"斌",3:"勇"}

//数组的长度会自动根据最大的索引来计算
e := [...]string{4:"郭"}

Slice 切片

Slice(切片)代表变长的序列,序列中每个元素都有相同的类型。一个slice类型一般写作 []T ,其中 T 代表 slice 中元素的类型; slice 的语法和数组很像,只是没有固定长度而已。

数组和slice之间有着紧密的联系。一个slice是一个轻量级的数据结构,提供了访问数组子序列(或者全部)元素的功能,而且slice的底层确实引用一个数组对象。一个slice由三个部分构成:指针、长度和容量。指针指向第一个slice元素对应的底层数组元素的地址,要注意的是slice的第一个元素并不一定就是数组的第一个元素。长度对应slice中元素的数目;长度不能超过容量,容量一般是从slice的开始位置到底层数据的结尾位置。内置的len和cap函数分别返回slice的长度和容量;

多个slice之间可以共享底层的数据,并且引用的数组部分区间可能重叠。

slice的切片操作 s[i:j] ,其中 0 <= i <= j <= cap(s),用于创建一个新的slice,引用s的从第i个元素开始到第j-1个元素的子序列。新的slice将只有 j-i 个元素。如果i位置的索引被省略的话,将使用0代替,如果j位置的索引被省略的话将使用 len(s) 代替。

示例代码如下:

months := [...]string{1:"January",/* ... */,12:"December"}
Q2 := months[4:7]
summer := months[6:9]

fmt.Println(Q2)   //["April" "May" "June"]
fmt.Println(summer)     //["June" "July" "August"]

示例图如下:

切片概念示意图

如果切片s操作超出 cap(s) 的上限将导致一个panic异常,但是超出 len(s) 则是意味着扩展了 slice,因为新的 slice 的长度会变大;

在初始化 slice 时(如:s := []int{0,1,2})会隐式地创建一个合适大小的数组,然后 slice 的指针指向底层数组;和数组字面值一样,slice的字面值也可以按顺序指定初始化序列,或者是通过索引和元素值指定,或者是两处风格的混合语法初始化;

和数组不同的是,slice之间不能比较,因此我们不能使用 == 操作符来判断两个 slice 是否含有全部相等元素。不过标准库提供了高度优化的 bytes.Equal 函数来判断两个字节型 slice 是否相等([]byte),但是对于其它类型的 slice ,我们必须自己展开每个元素进行比较;

slice不支持比较运算符,原因有2个:

  • 一个slice的元素是间接引用的,一个slice切到可以包含自身。虽然有很多办法处理这种情形,但是没有一个是简单有效的。
  • 因为slice的元素是间接引用的,一个固定值的slice在不同的时间可能包含不同的元素,因为底层数组的元素可能会被修改。并且Go语言中map等哈希表之类的数据结构的key只做简单的浅拷贝,它要求在整个声明周期中相等的key必须对相同的元素。对于像指针或chan之类的引用类型, == 相等测试可以判断两个是否是引用相同的对象。一个针对slice的浅相等测试的==操作符可能是有一定用处的,也能临时解决map类型的key问题,但是slice和数组不同的相等测试行为会让人困惑。因此,安全的做法是直接禁止slice之间的比较操作。

sice唯一合法的比较操作是和nil比较;

一个空的slice等于nil。一个nil值的slice并没有底层数组。一个nil值的slice的长度和容量都是0,但是也有非nil值的slice的长度和容量也是0的,例如 []int{}make([]int,3)[3:] 。与任意类型的nil值一样,我们可以用 []int(nil) 类型转换表达式来生成一个对应类型slice的nil值;

验证代码如下:

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

如果需要测试一个slice是否是空的,使用 len(s)==0 来判断,而不应用用 s == nil 来判断;除了和 nil 相等比较外,一个 nil 值的 slice 的行为和其它任意 0 长度的 slice 一样;例如 reverse(nil) 也是安全的。除了文档已经明确说明的地方,所有的 Go 语言函数应该以相同的 方式对待 nil 值的 slice 和 0 长度的 slice。

内置的 make 函数创建一个指定元素类型、长度和容量的 slice。容量部分可以省略,在这种 情况下,容量将等于长度。

make([]T, len)
make([]T, len, cap) // same as make([]T, cap)[:len]

在底层,make 创建了一个匿名的数组变量,然后返回一个 slice;只有通过返回的 slice 才能 引用底层匿名的数组变量。在第一种语句中,slice 是整个数组的 view。在第二个语句中, slice 只引用了底层数组的前 len 个元素,但是容量将包含整个的数组。额外的元素是留给未来的增长用的。

append 函数对于理解 slice 底层是如何工作的非常重要。
下面的 appendInt 函数,演示了 append 函数的原理:

func appendInt(x []int, y int) []int  {
    var z []int
    zlen := len(x) + 1

    if zlen <= cap(x) {
        z = x[:zlen]
    } else {
        
        zcap := zlen

        if zcap <= 2*len(x) {
            zcap = 2*len(x)
        }
        
        z = make([]int, zlen, zcap)
        copy(z, x)
        
    }
    
    z[len(x)] = y
    return z
}

每次调用 appendInt 函数,必须先检测 slice 底层数组是否有足够的容量来保存新添加的元 素。如果有足够空间的话,直接扩展 slice(依然在原有的底层数组之上),将新添加的 y 元素 复制到新扩展的空间,并返回 slice。因此,输入的 x 和输出的 z 共享相同的底层数组。

如果没有足够的增长空间的话,appendInt 函数则会先分配一个足够大的 slice 用于保存新的 结果,先将输入的 x 复制到新的空间,然后添加 y 元素。结果 z 和输入的 x 引用的将是不同 的底层数组。

虽然通过循环复制元素更直接,不过内置的 copy 函数可以方便地将一个 slice 复制另一个相 同类型的 slice。copy 函数的第一个参数是要复制的目标 slice,第二个参数是源 slice,目标 和源的位置顺序和 dst = src 赋值语句是一致的。两个 slice 可以共享同一个底层数组, 甚至有重叠也没有问题。copy 函数将返回成功复制的元素的个数(我们这里没有用到),等于两个 slice 中较小的长度,所以我们不用担心覆盖会超出目标 slice 的范围。

为了提高内存使用效率,新分配的数组一般略大于保存 x 和 y 所需要的最低大小。通过在每 次扩展数组时直接将长度翻倍从而避免了多次内存分配,也确保了添加单个元素操的平均时 间是一个常数时间。

内置的 append 函数可能使用比 appendInt 有更复杂的内存扩展策略。因此,通常我们并不知道 append 调用是否导致了内存的重新分配,因此我们也不能确认新的 slice 和原始的 slice 是否 引用的是相同的底层数组空间。同样,我们不能确认在原先的 slice 上的操作是否会影响到新 的 slice。因此,通常是将 append 返回的结果直接赋值给输入的 slice 变量:

runes = append(runes, r)

更新 slice 变量不仅对调用 append 函数是必要的,实际上对应任何可能导致长度、容量或底 层数组变化的操作都是必要的。要正确地使用 slice,需要记住尽管底层数组的元素是间接访 问的,但是 slice 对应结构体本身的指针、长度和容量部分是直接访问的。要更新这些信息需 要像上面例子那样一个显式的赋值操作。从这个角度看,slice 并不是一个纯粹的引用类型, 它实际上是一个类似下面结构体的聚合类型:

type IntSlice struct {
    prt *int
    len,cap int
}

Map

在Go语言中,一个map就是一个哈希表的引用,map类型可以写为map[K]V,其中K和V分别对应key和value。map中所有的key都有相同的类型,所有的value也有着相同的类型,但是key和value之间可以是不峡的数据类型。其中K对应的key必须是支持 == 比较运算符的数据类型,所以map可以通过测试key是否相等来判断是否已经存在。虽然浮点数类型民鼐是支持相等运算比较的,但是将浮点数用做key类型则是一个不好的想法,最坏的情况是可能出现的NaN和任何浮点数都不相等。对于V对应的value数据类型测没有任何的限制。

map 中的元素并不是一个变量,因此我们不能对 map 的元素进行取址操作,禁止对 map 元素取址的原因是 map 可能随着元素数量的增长而重新分配更大的内存空间, 从而可能导致之前的地址无效。

Map 的迭代顺序是不确定的,并且不同的哈希函数实现可能导致不同的遍历顺序。在实践 中,遍历的顺序是随机的,每一次遍历的顺序都不相同。这是故意的,每次都使用随机的遍历顺序可以强制要求程序不会依赖具体的哈希函数实现。如果要按顺序遍历 key/value 对,我们必须显式地对 key 进行排序,可以使用 sort 包的 Strings 函数对字符串 slice 进行排序。

map 类型的零值是 nil,也就是没有引用任何 哈希表。

var gby map[string]int
fmt.Println(gby == nil)     //  "true"
fmt.Println(len(gby) == 0)  // "true"

map 上的大部分操作,包括查找、删除、len 和 range 循环都可以安全工作在 nil 值的 map 上,它们的行为和一个空的 map 类似。但是向一个 nil 值的 map 存入元素将导致一个 panic 异常;

通过 key 作为索引下标来访问 map 将产生一个 value。如果 key 在 map 中是存在的,那么将 得到与 key 对应的 value;如果 key 不存在,那么将得到 value 对应类型的零值。这个规则很实用,但是有时候可能需要知道对应的元素是否真的 是在 map 之中。例如,如果元素类型是一个数字,你可以需要区分一个已经存在的 0,和不存在而返回零值的 0,可以像下面这样测试:

dd,ok := gby["dd"]
if  !ok {
//  代码
}

或者:

if dd,ok := gby["dd"]; !ok {
//  代码
}

在这种场景下,map 的下标语法将产生两个值;第二个是一个布尔值,用于报告元素是否真的存在。布尔变量一般命名为 ok,特别适合马上用于 if 条件判断部分。

和 slice 一样,map 之间也不能进行相等比较;唯一的例外是和 nil 进行比较。要判断两个 map 是否包含相同的 key 和 value,我们必须通过一个循环实现;

map的类型

语法:

map[key的类型]value的类型

map字面量语法

//创建一个空的map实例
a := map[string]int{}

//创建一个map实例,并且指定初始的key和value
b := map[string]int{"郭斌勇":28,"闯":18}

结构体

结构体是一种聚合的数据类型,是由零个或多个任意类型的值聚合成的实体。每个值称为结
构体的成员。

结构体类型的变量的成员也是变量,所以可以对结构体变量的成员取地址,然后通过指针访问;

type Employee struct {
    ID int
    Name string
    Address string
    DoB time.Time
    Position string
    Salary int
    ManagerID int
} 
var dilbert Employee



position := &dilbert.Position
*position = "Senior " + *position // promoted, for outsourcing to Elbonia

点操作符也可以和指向结构体的指针一起工作:

var employeeOfTheMonth *Employee = &dilbert
employeeOfTheMonth.Position += " (proactive team player)"

相当于下面语句

(*employeeOfTheMonth).Position += " (proactive team player)"

当函数的返回值是结构体类型的指针时,在调用该函数后可以直接访问该函数返回的结构体的成员,如:

func EmployeeByID(id int) *Employee { /* ... */ }
fmt.Println(EmployeeByID(dilbert.ManagerID).Position) // "Pointy-haired boss"
id := dilbert.ID
EmployeeByID(id).Salary = 0 // fired for... no real reason

但是,当函数的返回值是结构体类型的值时,则不能在调用该函数后直接访问该函数返回的结构体的成员;

在定义结构体类型时,通常一行对应一个结构体成员,成员的名字在前类型在后,不过如果相邻的成员类型如果相同的话可以被合并到一行,就像下面的Name和Address成员那样:

type Employee struct {
    ID int
    Name, Address string
    DoB time.Time
    Position string
    Salary int
    ManagerID int
}

结构体成员的输入顺序也有重要的意义。我们也可以将Position成员合并(因为也是字符串类
型) ,或者是交换Name和Address出现的先后顺序,那样的话就是定义了不同的结构体类
型。通常,我们只是将相关的成员写到一起。

如果结构体成员名字是以大写字母开头的,那么该成员就是导出的;这是Go语言导出规则决
定的。一个结构体可能同时包含导出和未导出的成员。

一个命名为S的结构体类型将不能再包含S类型的成员:因为一个聚合的值不能包含它自身。
(该限制同样适应于数组。) 但是S类型的结构体可以包含 *S 指针类型的成员,这可以让我
们创建递归的数据结构,比如链表和树结构等。

结构体类型的零值是每个成员都是对应类型的零值。通常会将零值作为最合理的默认值。

如果结构体没有任何成员的话就是空结构体,写作struct{}。它的大小为0,也不包含任何信
息,但是有时候依然是有价值的。有些Go语言程序员用map来模拟set数据结构时,用它来代
替map中布尔类型的value,只是强调key的重要性,但是因为节约的空间有限,而且语法比较
复杂,所有我们通常避免这样的用法。

seen := make(map[string]struct{}) // set of strings
// ...
if _, ok := seen[s]; !ok {
seen[s] = struct{}{}
// ...first time seeing s...
}

结构体面值

结构体值也可以用结构体面值表示,结构体面值可以指定每个成员的值。

type Point struct{ X, Y int }
p := Point{1, 2}

这里有两种形式的结构体面值语法,上面的是第一种写法,要求以结构体成员定义的顺序为
每个结构体成员指定一个面值。它要求写代码和读代码的人要记住结构体的每个成员的类型
和顺序,不过结构体成员有细微的调整就可能导致上述代码不能编译。因此,上述的语法一
般只在定义结构体的包内部使用,或者是在较小的结构体中使用,这些结构体的成员排列比
较规则,比如image.Point{x, y}或color.RGBA{red, green, blue, alpha}。

其实更常用的是第二种写法,以成员名字和相应的值来初始化,可以包含部分或全部的成
员,如之前的Lissajous程序的写法:

anim := gif.GIF{LoopCount: nframes}

在这种形式的结构体面值写法中,如果成员被忽略的话将默认用零值。因为,提供了成员的名字,所有成员出现的顺序并不重要。

两种不同形式的写法不能混合使用。而且,你不能企图在外部包中用第一种顺序赋值的技巧
来偷偷地初始化结构体中未导出的成员。

包p:

package p
type T struct{ a, b int } // a and b are not exported

包q:

package q
import "p"
var _ = p.T{a: 1, b: 2} // compile error: can't reference a, b
var _ = p.T{1, 2} // compile error: can't reference a, b

如果考虑效率的话,较大的结构体通常会用指针的方式传入和返回,可以用下面的写法来创建并初始化一个结构体变量,并返回结构体的地址:

pp := &Point{1, 2}

它是下面的语句是等价的:

pp := new(Point)
*pp = Point{1, 2}

结构体比较

如果结构体的全部成员都是可以比较的,那么结构体也是可以比较的,那样的话两个结构体
将可以使用==或!=运算符进行比较。相等比较运算符==将比较两个结构体的每个成员,因此
下面两个比较的表达式是等价的:

type Point struct{ X, Y int }
p := Point{1, 2}
q := Point{2, 1}
fmt.Println(p.X == q.X && p.Y == q.Y) // "false"
fmt.Println(p == q)

可比较的结构体类型和其他可比较的类型一样,可以用于map的key类型。

type address struct {
hostname string
port int
} h
its := make(map[address]int)
hits[address{"golang.org", 443}]++

结构体嵌入和匿名成员

在定义结构体的类型时,如果只声明一个成员对应的数据类型而不指定成员的名字,则称这类成员为匿名成员。匿名成员的数据类型必须是命名的类型或指向一个命名的类型的指针;

对于结构体类型的变量,可以直接主访问匿名成员的成员,示例如下:

type Point struct {
    X, Y int
}

type Circle struct {
    Point
    Radius int
}
type Wheel struct {
    Circle
    Spokes int
}


var w Wheel
w.X = 8     // 等效方式 w.Circle.Point.X = 8
w.Y = 8      // 等效方式 w.Circle.Point.Y = 8
w.Radius = 5 // 等效方式 w.Circle.Radius = 5
w.Spokes = 20

在右边的注释中给出的显式形式访问这些叶子成员的语法依然有效,因此匿名成员并不是真
的无法访问了。其中匿名成员Circle和Point都有自己的名字——就是命名的类型名字——但是
这些名字在点操作符中是可选的。我们在访问子成员的时候可以忽略任何匿名成员部分。

不幸的是,结构体字面值并没有简短表示匿名成员的语法, 因此下面的语句都不能编译通
过:

w = Wheel{8, 8, 5, 20} // compile error: unknown fields
w = Wheel{X: 8, Y: 8, Radius: 5, Spokes: 20} // compile error: unknown fields

结构体字面值必须遵循形状类型声明时的结构,所以我们只能用下面的两种语法,它们彼此
是等价的:

w = Wheel{Circle{Point{8, 8}, 5}, 20}
w = Wheel{
    Circle: Circle{
        Point:  Point{X: 8, Y: 8}, 
        Radius: 5,
},
Spokes: 20, // NOTE: trailing comma necessary here (and at Radius)
}
fmt.Printf("%#v\n", w)
// Output:
// Wheel{Circle:Circle{Point:Point{X:8, Y:8}, Radius:5}, Spokes:20}
w.X = 42
fmt.Printf("%#v\n", w)
// Output:
// Wheel{Circle:Circle{Point:Point{X:42, Y:8}, Radius:5}, Spokes:20}

需要注意的是Printf函数中%v参数包含的#副词,它表示用和Go语言类似的语法打印值。对于
结构体类型来说,将包含每个成员的名字。

因为匿名成员也有一个隐式的名字,因此不能同时包含两个类型相同的匿名成员,这会导致
名字冲突。同时,因为成员的名字是由其类型隐式地决定的,所以匿名成员也有可见性的规
则约束。在上面的例子中,Point和Circle匿名成员都是导出的。即使它们不导出(比如改成小
写字母开头的point和circle) ,我们依然可以用简短形式访问匿名成员嵌套的成员。

但是在包外部,因为circle和point没有导出不能访问它们的成员,因此简短的匿名成员访问语
法也是禁止的。

匿名成员特性只是对访问嵌套成员的点运算符提供了简短的语法糖;匿名成员并不要求是结构体类型;其实任何命名的类型都可以作为结构体的匿名成员,简短的点运算符语法可以用于选择匿名成员嵌套的成员,也可以用于访问它们的方法。实际上,外层的结构体不仅仅是获得了匿名成员类型的所有成员,而且也获得了该类型导出的全部的方法。这个机制可以用于将一个有简单行为的对象组合成有
复杂行为的对象。组合是Go语言中面向对象编程的核心;

JSON

JSON是对JavaScript中各种类型的值——字符串、数字、布尔值和对象——Unicode本文编码。基本的JSON类型有数字(十进制或科学记数法) 、布尔值(true或false) 、字符串,其中字符串是以双引号包含的Unicode字符序列,支持和Go语言类似的反斜杠转义特性,不过JSON使用的是\Uhhhh转义数字来表示一个UTF-16编码,而不是Go语言的rune类型。

这些基础类型可以通过JSON的数组和对象类型进行递归组合。一个JSON数组是一个有序的
值序列,写在一个方括号中并以逗号分隔;一个JSON数组可以用于编码Go语言的数组和slice。一个JSON对象是一个字符串到值的映射,写成以系列的name:value对形式,用花括号包含并以逗号分隔;JSON的对象类型可以用于编码Go语言的map类型(key类型是字符串)和结构体。

在编码时,默认使用Go语言结构体的成员名字作为JSON的对象(通过reflect反射技术)。只有导出的结构体成员才会被编码。

一个结构体成员Tag是在编译阶段关联到该成员的元信息字符串:

Year int `json:"released"`
Color bool `json:"color,omitempty"`

结构体的成员Tag可以是任意的字符串面值,但是通常是一系列用空格分隔的key:"value"键值
对序列;因为值中含义双引号字符,因此成员Tag一般用原生字符串面值的形式书写。json开
头键名对应的值用于控制encoding/json包的编码和解码的行为,并且encoding/...下面其它的
包也遵循这个约定。成员Tag中json对应值的第一部分用于指定JSON对象的名字;

示例如下:将Go语言中的TotalCount成员对应到JSON中的total_count对象。Color成员的Tag还带了一个额外
的omitempty选项,表示当Go语言结构体成员为空或零值时不生成JSON对象(这里false为零
值) ;

type Movie struct {
    Title string
    Year int `json:"released"`
    Color bool `json:"color,omitempty"`
    Actors []string
}

编码的逆操作是解码,对应将JSON数据解码为Go语言的数据结构,Go语言中一般叫
unmarshaling,通过json.Unmarshal函数完成。在解码时,只会解码Go语言数据结构中包含的成员,没有被包含的成员将被忽略;也可以使用 Tag 来指定对应JSON数据中的成员名字;

文本和HTML模板

一个模板是一个字符串或一个文件,里面包含了一个或多个由双花括号包含的 {{action}} 对象。大部分的字符串只是按面值打印,但是对于actions部分将触发其它的行为。每个actions都包含了一个用模板语言书写的表达式,一个action虽然简短但是可以输出复杂的打印值,模板语言包含通过选择结构体的成员、调用函数或方法、表达式控制流if-else语句和range循环语句,还有其它实例化模板等诸多特性。

对于每一个action,都有一个当前值的概念,对应点操作符,写作“.”。当前值“.”最初被初始化为调用模板是的参数;模板中 {{range .Items}}{{end}} 对应一个循环action,因此它们直接的内容可能会被展开多次,循环每次迭代的当前值对应当前的Items元素的值。

在一个action中, | 操作符表示将前一个表达式的结果作为后一个函数的输入,类似于UNIX中管道的概念。

下面是一个简单的模板字符串:

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

生成模板的输出需要两个处理步骤。第一步是要分析模板并转为内部表示,然后基于指定的输入执行模板。分析模板部分一般只需要执行一次。下面的代码创建并分析上面定义的模板templ。注意方法调用链的顺序:template.New先创建并返回一个模板;Funcs方法将daysAgo等自定义函数注册到模板中,并返回模板;最后调用Parse函数分析模板。

如下:

report, err := template.New("report").
Funcs(template.FuncMap{"daysAgo": daysAgo}).
Parse(templ)
if err != nil {
log.Fatal(err)
}

因为模板通常在编译时就测试好了,如果模板解析失败将是一个致命的错误。template.Must 辅助函数可以简化这个致命错误的处理:它接受一个模板和一个error类型的参数,检测error是否为nil(如果不是nil则发出panic异常) ,然后返回传入的模板。

html/template 使用和 text/template 包相同的API和模板语言,但是增加了一个将字符串自动转义特性,这可以避免输入字符串和HTML、JavaScript、CSS或URL语法产生冲突的问题。这个特性还可以避免一些长期存在的安全问题,比如通过生成HTML注入攻击,通过构造一个含有恶意代码的问题标题,这些都可能让模板输出错误的输出,从而让他们控制页面。

函数

函数可以让我们将一个语句序列打包为一个单元,然后可以从程序中其它地方多次调用。函数的机制可以让我们将一个大的工作分解为小的任务,这样的小任务可以让不同程序员在不同时间、不同地方独立完成。一个函数同时对用户隐藏了其实现细节。由于这些因素,对于任何编程语言来说,函数都是一个至关重要的部分。

函数声明

函数声明包括函数名、形式参数列表、返回值列表(可省略) 以及函数体。

func name(parameter-list) (result-list) {
    body
}

形式参数列表描述了函数的参数名以及参数类型。这些参数作为局部变量,其值由参数调用者提供。返回值列表描述了函数返回值的变量名以及类型。如果函数返回一个无名变量或者没有返回值,返回值列表的括号是可以省略的。如果一个函数声明不包括返回值列表,那么函数体执行完毕后,不会返回任何值。如果一组形参或返回值有相同的类型,我们不必为每个形参都写出参数类型。

如下:
在hypot函数中,

func hypot(x, y float64) float64 {
    return math.Sqrt(x*x + y*y)
}

fmt.Println(hypot(3,4)) // "5"

x和y是形参名,3和4是调用时的传入的实数,函数返回了一个float64类型的值。 返回值也可以像形式参数一样被命名。在这种情况下,每个返回值被声明成一个局部变量,并根据该返回值的类型,将其初始化为零值。 如果一个函数在声明时,包含返回值列表,该函数必须以 return 语句结尾,除非函数明显无法运行到结尾处。例如函数在结尾时调用了panic异常或函数中存在无限循环。

下面给出4种方法声明拥有2个int型参数和1个int型返回值的函数 .blank identifier(译者
注:即下文的_符号)可以强调某个参数未被使用

func add(x int, y int) int {return x + y}
func sub(x, y int) (z int) { z = x - y; return}
func first(x int, _ int) int { return x }
func zero(int, int) int { return 0 }


fmt.Printf("%T\n", add) // "func(int, int) int"
fmt.Printf("%T\n", sub) // "func(int, int) int"
fmt.Printf("%T\n", first) // "func(int, int) int"
fmt.Printf("%T\n", zero) // "func(int, int) int"

函数的类型被称为函数的标识符。如果两个函数形式参数列表和返回值列表中的变量类型一一对应,那么这两个函数被认为有相同的类型和标识符。形参和返回值的变量名不影响函数标识符也不影响它们是否可以以省略参数类型的形式表示。

每一次函数调用都必须按照声明顺序为所有参数提供实参(参数值)。在函数调用时,Go语言没有默认参数值,也没有任何方法可以通过参数名指定形参,因此形参和返回值的变量名对于函数调用者而言没有意义。

在函数体中,函数的形参作为局部变量,被初始化为调用者提供的值。函数的形参和有名返回值作为函数最外层的局部变量,被存储在相同的词法块中。

实参通过值的方式传递,因此函数的形参是实参的拷贝。对形参进行修改不会影响实参。但是,如果实参包括引用类型,如指针,slice(切片)、map、function、channel等类型,实参可能会由于函数的间接介引用被修改。

你可能会偶尔遇到没有函数体的函数声明,这表示该函数不是以Go实现的。这样的声明定义
了函数标识符。

func Sin(x float64) float //implemented in assembly language

大部分编程语言使用固定大小的函数调用栈,常见的大小从64KB到2MB不等。固定大小栈会限制递归的深度,当你用递归处理大量数据时,需要避免栈溢出;除此之外,还会导致安全性问题。与之相反,Go语言使用可变栈,栈的大小按需增加(初始时很小)。这使得我们使用递归时不必考虑溢出和安全问题。

多返回值

调用多返回值函数时,返回给调用者的是一组值,调用者必须显式的将这些值分配给变量:

links, err := findLinks(url)

如果某个值不被使用,可以将其分配给 blank identifier:

links, _ := findLinks(url) // errors ignored

一个函数内部可以将另一个有多返回值的函数的返回值作为返回值;

当你调用接受多参数的函数时,可以将一个返回多参数的函数作为该函数的参数。虽然这很
少出现在实际生产代码中,但这个特性在debug时很方便,我们只需要一条语句就可以输出所
有的返回值。下面的代码是等价的:

log.Println(findLinks(url))
links, err := findLinks(url)
log.Println(links, err)

如果一个函数定义了所有返回值的变量名,那么该函数的return语句可以省略操作数。这
称之为bare return。

// CountWordsAndImages does an HTTP GET request for the HTML
// document url and returns the number of words and images in it.
func CountWordsAndImages(url string) (words, images int, err error) {
    resp, err := http.Get(url)
    if err != nil {
        return
    } 
    doc, err := html.Parse(resp.Body)
    resp.Body.Close()
    if err != nil {
        err = fmt.Errorf("parsing HTML: %s", err)
        return
    } 
    words, images = countWordsAndImages(doc)
    return
} 
func countWordsAndImages(n *html.Node) (words, images int) { /* ... */ }

按照返回值列表的次序,返回所有的返回值,在上面的例子中,每一个return语句等价于:

return words, images, err

函数值

在Go中,函数被看作第一类值(first-class values) :函数像其他值一样,拥有类型,可以被赋值给其他变量,传递给函数,从函数返回。对函数值(function value) 的调用类似函数调用。

函数类型的零值是nil。调用值为nil的函数值会引起panic错误;函数值可以与nil比较,但是函数值之间是不可比较的,也不能用函数值作为map的key。

匿名函数

拥有函数名的函数只能在包级语法块中被声明,通过函数字面量(function literal) ,我们可绕过这一限制,在任何表达式中表示一个函数值。函数字面量的语法和函数声明相似,区别在于func关键字后没有函数名。函数值字面量是一种表达式,它的值被称为匿名函数(anonymous function) 。

函数字面量允许我们在使用函数时,再定义它;更为重要的是,通过函数字面量定义的函数可以访问完整的词法环境(lexical environment) ,这意味着在函数中定义的内部函数可以引用该函数的变量。

函数值是引用类型,这也是函数值不可比较的原因;Go使用闭包(closures) 技术实现函数值,Go程序员也把函数值叫做闭包。

当匿名函数需要被递归调用时,必须首先声明一个变量 ,再将匿名函数赋值给这个变量。如果不分成两部,函数字面量无法与该变量绑定(即:在函数字面量中无法正确访问保存该字面量值的变量),所以也就无法递归调用该匿名函数;

错误的示例如下:

visitAll := func(items []string) {
    // ...
    visitAll(m[item]) // compile error: undefined: visitAll
    // ...
}

警告:捕获迭代变量

    var funSlice []func()

    for _, item := range itemSlice {
        funSlice = append(funSlice, func() {
            fmt.Println(item)
        })
    }

    
    for _, fun := funSlice {
        fun()
    }

在上面的程序中,for循环语句引入了新的词法块,循环变量 item 在这个词法块中被声明。在该循环中生成的所有函数值都共享相同的循环变量 item 。需要注意,函数值中记录的是循环变量的内存地址,而不是循环变量某一时刻的值。以 item 为例,后续的迭代会不断更新 item 的值,当删除操作执行时,for 循环已完成,item 中存储的值等于最后一次迭代的值。这意味着,每次执行 fmt.Println(item) 时,都会输出相同的值;

通过在循环体的词法域中声明一个局部变量来保存 item 便可以解决此问题;如下:

    var funSlice []func()

    for _, item := range itemSlice {
        temItem := item
        funSlice = append(funSlice, func() {
            fmt.Println(temItem)
        })
    }

    
    for _, fun := funSlice {
        fun()
    }

这个问题不仅存在基于range的循环,对于在for循环中的初始化语句中声明的变量也会存在些问题;如下:

    var funSlice []func()

    for i := 0; i < count; i++  {
        funSlice = append(funSlice, func() {
            fmt.Println(i)
        })
    }

    
    for _, fun := funSlice {
        fun()
    }

可变参数

参数数量可变的函数称为为可变参数函数。在声明可变参数函数时,需要在参数列表的最后一个参数类型之前加上省略符号“...”,这表示该函数会接收任意数量的该类型参数。

func sum(vals...int) int {
    total := 0
    for _, val := range vals {
        total += val
    } 
    return total
}



    fmt.Println(sum()) // "0"
    fmt.Println(sum(3)) // "3"
    fmt.Println(sum(1, 2, 3, 4)) // "10"

sum函数返回任意个int型参数的和。在函数体中,vals被看作是类型为 []int 的切片。sum可以接收任意数量的int型参数;

在上面的代码中,调用者隐式的创建一个数组,并将原始参数复制到数组中,再把数组的一个切片作为参数传给被调函数。如果原始参数已经是切片类型,我们该如何传递给sum?只需在最后一个参数后加上省略符。下面的代码功能与上个例子中最后一条语句相同。

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

虽然在可变参数函数内部,...int 型参数的行为看起来很像切片类型,但实际上,可变参数函
数和以切片作为参数的函数是不同的:

func f(...int) {}
func g([]int) {}
fmt.Printf("%T\n", f) // "func(...int)"
fmt.Printf("%T\n", g) // "func([]int)"

Deferred 函数

你只需要在调用普通函数或方法前加上关键字defer,就完成了defer所需要的语法。当defer语句被执行时,跟在defer后面的函数会被延迟执行。直到包含该defer语句的函数执行完毕时,defer后的函数才会被执行,不论包含defer语句的函数是通过return正常结束,还是由于panic导致的异常结束。你可以在一个函数中执行多条defer语句,它们的执行顺序与声明顺序相反。

defer语句中的函数会在return语句更新返回值变量后再执行,又因为在函数中定义的匿名函数可以访问该函数包括返回值变量在内的所有变量,所以,对匿名函数采用defer机制,可以使其观察函数的返回值。

如下所示:

func double(x int) (result int) {
    defer func() { fmt.Printf("double(%d) = %d\n", x,result) }()
    return x + x
} 
_ = double(4)
// Output:
// "double(4) = 8"

被延迟执行的匿名函数甚至可以修改函数返回给调用者的返回值:

func double(x int) (result int) {
    defer func() { fmt.Printf("double(%d) = %d\n", x,result) }()
    return x + x
} 
_ = double(4)
// Output:
// "double(4) = 8"

Panic异常

Go的类型系统会在编译时捕获很多错误,但有些错误只能在运行时检查,如数组访问越界、空指针引用等。这些运行时错误会引起painc异常。

一般而言,当panic异常发生时,程序会中断运行,并立即执行在该 goroutine(可以暂时理解成线程) 中被延迟的函数(defer 机制) 。随后,程序崩溃并输出日志信息。日志信息包括panic value和函数调用的堆栈跟踪信息。panic value通常是某种错误信息。对于每个goroutine,日志信息中都会有与之相对的,发生panic时的函数调用堆栈跟踪信息。通常,我们不需要再次运行程序去定位问题,日志信息已经提供了足够的诊断依据。因此,在我们填写问题报告时,一般会将panic异常和日志信息一并记录。

不是所有的panic异常都来自运行时,直接调用内置的panic函数也会引发panic异常;panic函数接受任何值作为参数。当某些不应该发生的场景发生时,我们就应该调用panic。比如,当程序到达了某条逻辑上不可能到达的路径;

虽然Go的panic机制类似于其他语言的异常,但panic的适用场景有一些不同。由于panic会引起程序的崩溃,因此panic一般用于严重错误,如程序内部的逻辑不一致。勤奋的程序员认为任何崩溃都表明代码中存在漏洞,所以对于大部分漏洞,我们应该使用Go提供的错误机制,而不是panic,尽量避免程序的崩溃。在健壮的程序中,任何可以预料到的错误,如不正确的输入、错误的配置或是失败的I/O操作都应该被优雅的处理,最好的处理方式,就是使用Go的错误机制。

将panic机制类比其他语言异常机制的读者可能会惊讶,runtime.Stack为何能输出已经被释放函数的信息?在Go的panic机制中,延迟函数的调用在释放堆栈信息之前。

Recover 捕获异常

如果在deferred函数中调用了内置函数recover,并且定义该defer语句的函数发生了panic异常,recover会使程序从panic中恢复,并返回panic value。导致panic异常的函数不会继续运行,但能正常返回。在未发生panic时调用recover,recover会返回nil。

方法

尽管没有被大众所接受的明确的OOP的定义,从我们的理解来讲,一个对象其实也就是一个简单的值或者一个变量,在这个对象中会包含一些方法,而一个方法则是一个一个和特殊类型关联的函数。一个面向对象的程序会用方法来表达其属性和对应的操作,这样使用这个对象的用户就不需要直接去操作对象,而是借助方法来做这些事情。

方法声明

在函数声明时,在其名字之前放上一个变量,即是一个方法。这个附加的参数会将该函数附加到这种类型上,即相当于为这种类型定义了一个独占的方法。

示例:

package geometry

import "math"


type Point struct{ X, Y float64 }
// traditional function
func Distance(p, q Point) float64 {
    return math.Hypot(q.X-p.X, q.Y-p.Y)
}

// same thing, but as a method of the Point type
func (p Point) Distance(q Point) float64 {
    return math.Hypot(q.X-p.X, q.Y-p.Y)
}

上面的代码里那个附加的参数p,叫做方法的接收器(receiver),早期的面向对象语言留下的遗产将调用一个方法称为“向一个对象发送消息”。

在Go语言中,我们并不会像其它语言那样用this或者self作为接收器;我们可以任意的选择接收器的名字。

在方法调用过程中,接收器参数一般会在方法名之前出现。这和方法声明是一样的,都是接收器参数在方法名字之前。

下面是例子:

p := Point{1, 2}
q := Point{4, 6}
fmt.Println(Distance(p, q)) // "5", function call
fmt.Println(p.Distance(q)) // "5", method call

这种p.Distance的表达式叫做选择器,因为他会选择合适的对应p这个对象的Distance方法来
执行。选择器也会被用来选择一个struct类型的字段,比如p.X。由于方法和字段都是在同一
命名空间,所以如果我们在这里声明一个X方法的话,编译器会报错,因为在调用p.X时会有
歧义;

在能够给任意类型定义方法这一点上,Go和很多其它的面向对象的语言不太一样。因此在Go语言里,我们为一些简单的数值、字符串、slice、map来定义一些附加行为很方便。方法可以被声明到任意类型,只要不是一个指针或者一个interface ;

对于一个给定的类型,其内部的方法都必须有唯一的方法名,但是不同的类型却可以有同样的方法名;

指针类型的方法

方法的接收器的类型也可以是指针;
示例如下:

//定义新的类型 T
type T struct {
    Name string
    Age int
}

//接收器的类型是 T 的指针类型 *T
func (g *T) PointerMethod() {

}


//接收器的类型是 T 类型
func (g T) TypeMethod()   {

}

对于方法 method ,无论接收器 receiver 的 实参 与 开参 是同种类型(都是 指针类型 或者 都是 非指针类型),还是不同的类型(分别是同种类型的 指针类型 和 非指针类型),都能通过直接调用的方式 receiver.method() 来调用;编译器帮我们进行会隐式的类型转换(解引用 或 取地址);

下面的表达式都是正确的:


    var gby T = T{Name:"郭斌勇",Age:27}
    
    gby.TypeMethod()
    gby.PointerMethod()     // 等效于: (&gby).PointerMethod()
    
    var ptr *T = &gby
    
    ptr.PointerMethod()
    ptr.TypeMethod()    // 等效于: (*ptr).TypeMethod()

Nil 也是一个合法的接收器类型

方法的接收器的值也可以是 nil ,但是,直接在 nil 字面量上调用方法是无法通过编译的, 因为 nil 的字面量经门充器无法推断其类型, 不过,可以在类型转换后面直接调用 T(nil).method();

通过嵌入结构体来扩展

对于结构体类型的变量,不但可以直接访问匿名成员的成员,也可以直接访问 和 调用匿名成员的方法;

示例如下:

type Point struct{ X, Y float64 }

type ColoredPoint struct { 
    Point
    Color color.RGBA
}

我们可以把 ColoredPoint 类型当作接收器来调用 Point 里的方法,即 ColoredPoint 里没有声明这些方法:

red := color.RGBA{255, 0, 0, 255}
blue := color.RGBA{0, 0, 255, 255}
var p = ColoredPoint{Point{1, 1}, red}
var q = ColoredPoint{Point{5, 4}, blue}
fmt.Println(p.Distance(q.Point)) // "5"
p.ScaleBy(2)
q.ScaleBy(2)
fmt.Println(p.Distance(q.Point)) // "10"

读者如果对基于类来实现面向对象的语言比较熟悉的话,可能会倾向于将Point看作一个基类,而ColoredPoint看作其子类或者继承类,或者将ColoredPoint看作"is a" Point类型。但这是错误的理解。请注意上面例子中对Distance方法的调用。Distance有一个参数是Point类型,但q并不是一个Point类,所以尽管q有着Point这个内嵌类型,我们也必须要显式地选择它。尝试直接传q的话你会看到下面这样的错误:

p.Distance(q) // compile error: cannot use q (ColoredPoint) as Point

一个ColoredPoint并不是一个Point,但他"has a"Point,并且它有从Point类里引入的Distance和ScaleBy方法。如果你喜欢从实现的角度来考虑问题,内嵌字段会指导编译器去生成额外的包装方法来委托已经声明好的方法,和下面的形式是等价的:

func (p ColoredPoint) Distance(q Point) float64 {
    return p.Point.Distance(q)
} 
func (p *ColoredPoint) ScaleBy(factor float64) {
    p.Point.ScaleBy(factor)
}

当Point.Distance被第一个包装方法调用时,它的接收器值是p.Point,而不是p,当然了,在Point类的方法里,你是访问不到ColoredPoint的任何字段的。

在类型中内嵌的匿名字段也可能是一个命名类型的指针,这种情况下字段和方法会被间接地引入到当前的类型中(译注:访问需要通过该指针指向的对象去取)。添加这一层间接关系让我们可以共享通用的结构并动态地改变对象之间的关系。下面这个ColoredPoint的声明内嵌了一个*Point的指针。

type ColoredPoint struct {
    *Point
    Color color.RGBA
} 


p := ColoredPoint{&Point{1, 1}, red}
q := ColoredPoint{&Point{5, 4}, blue}
fmt.Println(p.Distance(*q.Point)) // "5"
q.Point = p.Point // p and q now share the same Point
p.ScaleBy(2)
fmt.Println(*p.Point, *q.Point) // "{2 2} {2 2}"

一个struct类型也可能会有多个匿名字段。我们将ColoredPoint定义为下面这样:

type ColoredPoint struct {
    Point
    color.RGBA
}

然后这种类型的值便会拥有Point和RGBA类型的所有方法,以及直接定义在ColoredPoint中的方法。当编译器解析一个选择器到方法时,比如p.ScaleBy,它会首先去找直接定义在这个类型里的ScaleBy方法,然后找被ColoredPoint的内嵌字段们引入的方法,然后去找Point和RGBA的内嵌字段引入的方法,然后一直递归向下找。如果选择器有二义性的话编译器会报错,比如你在同一级里有两个同名的方法。

方法值和方法表达式

我们经常选择一个方法,并且在同一个表达式里执行,比如常见的p.Distance()形式,实际上将其分成两步来执行也是可能的。p.Distance叫作“选择器”,选择器会返回一个方法"值"->一个将方法(Point.Distance)绑定到特定接收器变量的函数。这个函数可以不通过指定其接收器即可被调用;即调用时不需要指定接收器(译注:因为已经在前文中指定过了),只要传入函数的参数即可:

p := Point{1, 2}
q := Point{4, 6}
distanceFromP := p.Distance // method value
fmt.Println(distanceFromP(q)) // "5"
var origin Point // {0, 0}
fmt.Println(distanceFromP(origin)) // "2.23606797749979", sqrt(5)
scaleP := p.ScaleBy // method value
scaleP(2) // p becomes (2, 4)
scaleP(3) // then (6, 12)
scaleP(10) // then (60, 120)

在一个包的API需要一个函数值、且调用方希望操作的是某一个绑定了对象的方法的话,方法"值"会非常实用;

当T是一个类型时,方法表达式可能会写作T.f或者(*T).f,会返回一个函数"值",这种函数会将其第一个参数用作接收器,所以可以用通常(译注:不写选择器)的方式来对其进行调用:

p := Point{1, 2}
q := Point{4, 6}

distance := Point.Distance // method expression
fmt.Println(distance(p, q)) // "5"
fmt.Printf("%T\n", distance) // "func(Point, Point) float64"


scale := (*Point).ScaleBy
scale(&p, 2)
fmt.Println(p) // "{2 4}"
fmt.Printf("%T\n", scale) // "func(*Point, float64)"
// 译注:这个Distance实际上是指定了Point对象为接收器的一个方法func (p Point) Distance(),
// 但通过Point.Distance得到的函数需要比实际的Distance方法多一个参数,
// 即其需要用第一个额外参数指定接收器,后面排列Distance方法的参数。
// 看起来本书中函数和方法的区别是指有没有接收器,而不像其他语言那样是指有没有返回值。

封装

Go语言只有一种控制可见性的手段:大写首字母的标识符会从定义它们的包中被导出,小写字母的则不会。这种基于名字的手段使得在语言中最小的封装单元是package,而不是像其它语言一样的类型。一个struct类型的字段对同一个包的所有代码都有可见性,无论你的代码是写在一个函数还是一个方法里。

只用来访问或修改内部变量的函数被称为setter或者getter,在命名一个getter方法时,我们通常会省略掉前面的Get前缀。

接口

一个类型可以自由的使用另一个满足相同接口的类型来进行替换被称作可替换性(LSP里氏替换)。这是一个面向对象的特征。

接口类型

接口类型具体描述了一系列方法的集合,一个实现了这些方法的具体类型是这个接口类型的实例。

定义接口的方式如下:

type Reader interface {
    Read(p []byte) (n int, err error)
} 
type Closer interface {
    Close() error
}

接口也可以像结构体那样通过内嵌来定义,这种定义方式称为接口内嵌;

下面3种定义方式都是一样的效果。方法的顺序变化也没有影响,唯一重要的就是这个集合里
面的方法:

方式1:

type ReadWriter interface {
    Reader
    Writer
}

方式2:

type ReadWriter interface {
    Read(p []byte) (n int, err error)
    Write(p []byte) (n int, err error)
}

方式3:

type ReadWriter interface {
    Read(p []byte) (n int, err error)
    Writer
}

实现接口的条件

一个类型如果拥有一个接口需要的所有方法,那么这个类型就实现了这个接口。

接口指定的规则非常简单:表达一个类型属于某个接口只要这个类型实现这个接口。所以:

var w io.Writer
w = os.Stdout // OK: *os.File has Write method
w = new(bytes.Buffer) // OK: *bytes.Buffer has Write method
w = time.Second // compile error: time.Duration lacks Write method

var rwc io.ReadWriteCloser
rwc = os.Stdout // OK: *os.File has Read, Write, Close methods
rwc = new(bytes.Buffer) // compile error: *bytes.Buffer lacks Close method

这个规则甚至适用于等式右边本身也是一个接口类型:

w = rwc // OK: io.ReadWriteCloser has Write method
rwc = w // compile error: io.Writer lacks Close method

因为ReadWriter和ReadWriteCloser包含所有Writer的方法,所以任何实现了ReadWriter和
ReadWriteCloser的类型必定也实现了Writer接口。

对于每一个命名过的具体类型T;它一些方法的接收者是类型T本身然而另一些则是一个T的指针。还记得在T类型的参数上调用一个T的方法是合法的,只要这个参数是一个变量;编译器隐式的获取了它的地址。但这仅仅是一个语法糖:T类型的值不拥有所有*T指针的方法,那这样它就可能只实现更少的接口。

举个例子可能会更清晰一点。在第6.5章中,*IntSet类型的String方法的接收者是一个指针类型,所以我们不能在一个不能寻址的IntSet值上调用这个方法:

type IntSet struct { /* ... */ }
func (*IntSet) String() string
var _ = IntSet{}.String() // compile error: String requires *IntSet receiver

但是我们可以在一个IntSet值上调用这个方法:

var s IntSet
var _ = s.String() // OK: s is a variable and &s has a String method

然而,由于只有IntSet类型有String方法,所有也只有IntSet类型实现了fmt.Stringer接口:

var _ fmt.Stringer = &s // OK
var _ fmt.Stringer = s // compile error: IntSet lacks String method

接口值

概念上讲一个接口的值,接口值,由两个部分组成,一个具体的类型和那个类型的值。它们被称为接口的动态类型和动态值。对于像Go语言这种静态类型的语言,类型是编译期的概念;因此一个类型不是一个值。在我们的概念模型中,一些提供每个类型信息的值被称为类型描述符,比如类型的名称和方法。在一个接口值中,类型部分代表与之相关类型的描述符。

在Go语言中,变量总是被一个定义明确的值初始化,即使接口类型也不例外。对于一个接口的零值就是它的类型和值的部分都是nil。

空接口值结构

一个接口值基于它的动态类型被描述为空或非空,所以这是一个空的接口值。你可以通过使用w==nil或者w!=nil来判读接口值是否为空。调用一个空接口值上的任意方法都会产生panic。

var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil

第二个语句将一个 *os.File 类型的值赋给变量w,这个赋值过程调用了一个具体类型到接口类型的隐式转换,这和显式的使用io.Writer(os.Stdout)是等价的。这类转换不管是显式的还是隐式的,都会刻画出操作到的类型和值。这个接口值的动态类型被设为 *os.Stdout 指针的类型描述符,它的动态值持有os.Stdout的拷贝;这是一个代表处理标准输出的os.File类型变量的指针:

接口值结构
w.Write([]byte("hello")) // "hello"

通常在编译期,我们不知道接口值的动态类型是什么,所以一个接口上的调用必须使用动态分配。因为不是直接进行调用,所以编译器必须把代码生成在类型描述符的方法Write上,然后间接调用那个地址。这个调用的接收者是一个接口动态值的拷贝,os.Stdout。效果和下面这个直接调用一样:

os.Stdout.Write([]byte("hello")) // "hello"

第四个语句将nil赋给了接口值:

w = nil

这个重置将它所有的部分都设为nil值,把变量w恢复到和它之前定义时相同的状态图;

一个接口值可以持有任意大的动态值。例如,表示时间实例的time.Time类型,这个类型有几个对外不公开的字段。我们从它上面创建一个接口值:

var x interface{} = time.Now()

结构图如下:

接口值的概念模型

从概念上讲,不论接口值多大,动态值总是可以容下它。(这只是一个概念上的模型;具体的实现可能会非常不同)

接口值可以使用==和!=来进行比较。两个接口值相等仅当它们都是nil值或者它们的动态类型相同并且动态值也根据这个动态类型的==操作相等。因为接口值是可比较的,所以它们可以用在map的键或者作为switch语句的操作数。

然而,如果两个接口值的动态类型相同,但是这个动态类型是不可比较的(比如切片) ,将它们进行比较就会失败并且panic;

当我们处理错误或者调试的过程中,得知接口值的动态类型是非常有帮助的。所以我们使用fmt包的%T动作:

var w io.Writer
fmt.Printf("%T\n", w) // "<nil>"
w = os.Stdout
fmt.Printf("%T\n", w) // "*os.File"
w = new(bytes.Buffer)
fmt.Printf("%T\n", w) // "*bytes.Buffer"

在fmt包内部,使用反射来获取接口动态类型的名称。

警告:一个包含nil指针的接口不是nil接口

一个不包含任何值的nil接口值和一个刚好包含nil指针的接口值是不同的。

如下:

var buf *bytes.Buffer
w = buf

给 w 赋了一个 *bytes.Buffer 的空指针,所以 w 的动态值是nil。然而,它的动态类型是 *bytes.Buffer,意思就是 w 变量是一个包含空指针值的非
空接口,如下,所以防御性检查out!=nil的结果依然是true。

值为空指针的接口值的结构

动态分配机制依然决定(bytes.Buffer).Write的方法会被调用,但是这次的接收者的值是nil。对于一些如os.File的类型,nil是一个有效的接收者(§6.2.1),但是 *bytes.Buffer 类型不在这些类型中。这个方法会被调用,但是当它尝试去获取缓冲区时会发生panic。

类型断言

类型断言是一个使用在接口值上的操作。语法上它看起来像x.(T)被称为断言类型,这里x表示一个接口的类型和T表示一个类型。一个类型断言检查它操作对象的动态类型是否和断言的类型匹配。

这里有两种可能。第一种,如果断言的类型T是一个具体类型,然后类型断言检查x的动态类型是否和T相同。如果这个检查成功了,类型断言的结果是x的动态值,当然它的类型是T。换句话说,具体类型的类型断言从它的操作对象中获得具体的值。如果检查失败,接下来这个操作会抛出panic。例如:

var w io.Writer
w = os.Stdout
f := w.(*os.File) // success: f == os.Stdout
c := w.(*bytes.Buffer) // panic: interface holds *os.File, not *bytes.Buffer

第二种,如果相反断言的类型T是一个接口类型,然后类型断言检查是否x的动态类型满足T。如果这个检查成功了,动态值没有获取到;这个结果仍然是一个有相同类型和值部分的接口值,但是结果有类型T。换句话说,对一个接口类型的类型断言改变了类型的表述方式,改变了可以获取的方法集合(通常更大) ,但是它保护了接口值内部的动态类型和值的部分。

在下面的第一个类型断言后,w和rw都持有os.Stdout因此它们每个有一个动态类型*os.File,但是变量w是一个io.Writer类型只对外公开出文件的Write方法,然而rw变量也只公开它的Read方法。

var w io.Writer
w = os.Stdout
rw := w.(io.ReadWriter) // success: *os.File has both Read and Write
w = new(ByteCounter)
rw = w.(io.ReadWriter) // panic: *ByteCounter has no Read method

如果断言操作的对象是一个nil接口值,那么不论被断言的类型是什么这个类型断言都会失败。我们几乎不需要对一个更少限制性的接口类型(更少的方法集合) 做断言,因为它表现的就像赋值操作一样,除了对于nil接口值的情况。

w = rw // io.ReadWriter is assignable to io.Writer
w = rw.(io.Writer) // fails only if rw == nil

经常地我们对一个接口值的动态类型是不确定的,并且我们更愿意去检验它是否是一些特定的类型。如果类型断言出现在一个预期有两个结果的赋值操作中,例如如下的定义,这个操作不会在失败的时候发生panic但是代替地返回一个额外的第二个结果,这个结果是一个标识成功的布尔值:

var w io.Writer = os.Stdout
f, ok := w.(*os.File) // success: ok, f == os.Stdout
b, ok := w.(*bytes.Buffer) // failure: !ok, b == nil

第二个结果常规地赋值给一个命名为ok的变量。如果这个操作失败了,那么ok就是false值。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269