02程序结构
一:命名
1:Go语言中的函数名、变量名、常量名、类型名、语句标号和包名等所有的命名,都遵循一个简单的标识符命名规则:一个名字必须以一个字母(Unicode字母)或下划线开头,后面可以跟任意数量的字母、数字或下划线。注意,这里的字母是指Unicode编码字母,因此Go语言开发者可以在代码中自由地使用他们的母语。
Go中的标识符区分大小写。
2:Go定义了若干关键字,标识符不能是这些关键字之一。Go定义的关键字如下:
break |
default |
func |
interface |
select |
case |
defer |
go |
map |
struct |
chan |
else |
goto |
package |
switch |
const |
fallthrough |
if |
range |
type |
continue |
for |
import |
return |
var |
Go还预定义了一些标识符,虽然定义与这些预定义标识符一样的标识符也能编译通过,但最好不这么做。预定义的标识符如下:
Types |
bool byte complex64 complex128 error float32 float64 int int8 int16 int32 int64 rune string uint uint8 uint16 uint32 uint64 uintptr |
Constants |
true false iota |
Zero value |
nil |
Functions |
append cap close complex copy delete imag len make new panic print println real recover |
3:空标识符”_”是一个占位符,在赋值操作的时候将某个值赋值给空标识符,从而达到丢弃该值的目的。
空标识符不是一个新的变量,因此将它用于”:=”操作符的时候,必须同时为至少另一个值赋值。
4:如果一个名字是在函数内部定义,那么它就只在函数内部有效。如果是在函数外部定义,那么将在当前包的所有文件中都可以访问。
名字的开头字母的大小写决定了名字在包外的可见性。如果一个名字是大写字母开头的(译注:必须是在函数外部定义的包级名字;包级函数名本身也是包级名字),那么它将是导出的,也就是说可以被外部的包访问,例如fmt包的Printf函数就是导出的,可以在fmt包外部访问。包本身的名字一般总是用小写字母。
5:Go语言的风格是尽量使用短小的名字。通常来说,如果一个名字的作用域比较大,生命周期也比较长,那么用长的名字将会更有意义。
6:在习惯上,Go语言程序员推荐使用 驼峰式 命名,当名字有几个单词组成的时优先使用大小写分隔,而不是优先用下划线分隔。因此,在标准库有QuoteRuneToASCII和parseRequestLine这样的函数命名。
二:声明
1:Go语言主要有四种类型的声明语句:var、const、type和func,分别对应变量、常量、类型和函数实体对象的声明。
2:一个Go语言编写的程序对应一个或多个以.go为文件后缀名的源文件中。每个源文件以包的声明语句开始,说明该源文件是属于哪个包。包声明语句之后是import语句导入依赖的其它包,然后是包一级的类型、变量、常量、函数的声明语句,包一级的各种类型的声明语句的顺序无关紧要(译注:函数内部的名字则必须先声明之后才能使用)。比如:
func main() { str := "hehe" fmt.Println(str) fmt.Println(globalInt) printfun() } func printfun() { fmt.Println("hello,world") } var globalInt = 3
上面的例子中,printfun函数和变量globalInt 在定义之前就使用了,这是没有问题的。但是在main中,如果在str定义之前就使用它,就会报语法错误。
三:变量
1:var声明语句可以创建一个特定类型的变量,然后给变量附加一个名字,并且设置变量的初始值。
变量声明的一般语法如下:“ var 变量名字 类型 = 表达式 ”。其中“类型”或“= 表达式”两个部分可以省略其中的一个。如果省略的是类型信息,那么将根据初始化表达式来推导变量的类型信息。如果初始化表达式被省略,那么将用零值初始化该变量。
数值类型变量对应的零值是0,布尔类型变量对应的零值是false,字符串类型对应的零值是空字符串,接口或引用类型(包括slice、map、chan和函数)变量对应的零值是nil。数组或结构体等聚合类型对应的零值是每个元素或字段都是对应该类型的零值。
零值初始化机制可以确保每个声明的变量总是有一个良好定义的值,因此在Go语言中不存在未初始化的变量。Go语言程序员应该让一些聚合类型的零值也具有意义,这样可以保证不管任何类型的变量总是有一个合理有效的零值状态。
2:可以在一个声明语句中同时声明一组变量,或用一组初始化表达式声明并初始化一组变量。如果省略每个变量的类型,将可以声明多个类型不同的变量(类型由初始化表达式推导):
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
3:在包级别声明的变量会在main入口函数执行前完成初始化,局部变量将在声明语句被执行到的时候完成初始化:
var globalInt = getvalue() func getvalue() int { fmt.Println("this is getvalue") return 3 } func main() { fmt.Printf("globalInt is %d\n", globalInt) }
结果如下:
this is getvalue globalInt is 3
4:在函数内部,可以以“简短变量声明”的形式声明和初始化局部变量,也就是“名字 := 表达式”的形式。变量的类型根据表达式来自动推导。
注意,这种形式只能用于局部变量,不能用于包级别变量;
同var形式声明一样,在“简短变量声明”中,可以使用字面量、表达式和函数返回值来初始化变量:也可以初始化一组变量:
//i := 3 func main() { i := getValue() j := 1 k := rand.Float64() * 3.0 m, n := 3.4, "abc" }
上面的代码中,如果把注释去掉,就会报编译错误:”syntax error: non-declaration statement outside function body”。
简短变量声明左边的变量不一定都是新声明的变量。如果有一些变量已经在相同的词法域声明过了,那么简短变量声明语句对这些已经声明过的变量就只有赋值行为了。需要注意的是,至少要有一个变量是新的,否则回报编译错误:
func getValue() (int,string) { return 3, "abc" } func main() { i := 2 //j := "hehe" i,j := getValue() }
如果去掉注释的话,回报编译错误:”no new variables on left side of :=”。
注意:简短变量声明语句只有对已经在同级词法域声明过的变量才和赋值操作语句等价,如果变量是在外部词法域声明的,那么简短变量声明语句将会在当前词法域重新声明一个新的变量。
5:普通变量在声明语句创建时被绑定到一个变量名,通过指针,我们可以直接读或更新对应变量的值,而不需要知道该变量的名字(如果变量有名字的话)。
类型*int表示指向int类型的指针。
任何类型的指针的零值都是nil。指针之间可以进行相等测试,只有当它们指向同一个变量或全部是nil时才相等。
在Go语言中,返回函数中局部变量的地址也是安全的。例如下面的代码,调用f函数时创建局部变量v,在局部变量地址被返回之后依然有效,因为指针p依然引用这个变量:
func f() *int { v := 1 return &v } func main() { var p = f() fmt.Println(p) fmt.Println(*p) *p = 3 fmt.Println(p) fmt.Println(*p) p = f() fmt.Println(p) fmt.Println(*p) }
每次调用f函数都将返回不同的结果,上述代码结果如下:
0xc420012098 1 0xc420012098 3 0xc4200120d0 1
每次对一个变量取地址,或者复制指针,我们都是为原变量创建了新的别名。指针特别有价值的地方在于我们可以不用名字而访问一个变量,但是这是一把双刃剑:要找到一个变量的所有访问者并不容易,我们必须知道变量全部的别名(译注:这是Go语言的垃圾回收器所做的工作)。不仅仅是指针会创建别名,很多其他引用类型也会创建别名,例如slice、map和chan,甚至结构体、数组和接口都会创建所引用变量的别名。
6:另一个创建变量的方法是调用用内建的new函数。表达式new(T)将创建一个T类型的匿名变量,初始化为T类型的零值,然后返回变量地址,返回的指针类型为*T。
p := new(int) // p, *int 类型, 指向匿名的 int 变量 fmt.Println(*p) // "0" *p = 2 // 设置 int 匿名变量的值为 2 fmt.Println(*p) // "2"
7:变量的生命周期指的是在程序运行期间变量有效存在的时间间隔。对于包一级声明的变量,它们的生命周期和整个程序的运行周期是一致的。而局部变量的生命周期则是动态的:从每次创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量的存储空间可能被回收。函数的参数变量和返回值变量都是局部变量。它们在函数每次被调用的时候创建。
那么Go语言的自动垃圾收集器是如何知道一个变量是何时可以被回收的呢?基本的实现思路是,从每个包级的变量和每个当前运行函数的每一个局部变量开始,通过指针或引用的访问路径遍历,是否可以找到该变量。如果不存在这样的访问路径,那么说明该变量是不可达的,也就是说它是否存在并不会影响程序后续的计算结果。
因为一个变量的有效周期只取决于是否可达,因此一个循环迭代内部的局部变量的生命周期可能超出其局部作用域。同时,局部变量可能在函数返回之后依然存在。
编译器会自动选择在栈上还是在堆上分配局部变量的存储空间,但可能令人惊讶的是,这个选择并不是由用var还是new声明变量的方式决定的:
var global *int func f() { var x int x = 1 global = &x } func g() { y := new(int) *y = 1 }
f函数里的x变量必须在堆上分配,因为它在函数退出后依然可以通过包一级的global变量找到,用Go语言的术语说,这个x局部变量从函数f中逃逸了;相反,当g函数返回时,变量*y将是不可达的,也就是说可以马上被回收的。因此,*y并没有从函数g中逃逸,编译器可以选择在栈上分配*y的存储空间,虽然这里用的是new方式。
其实在任何时候,你并不需为了编写正确的代码而要考虑变量的逃逸行为,要记住的是,逃逸的变量需要额外分配内存,同时对性能的优化可能会产生细微的影响。
Go语言的自动垃圾收集器对编写正确的代码是一个巨大的帮助,但也并不是说你完全不用考虑内存了。你虽然不需要显式地分配和释放内存,但是要编写高效的程序你依然需要了解变量的生命周期。例如,如果将指向短生命周期对象的指针保存到具有长生命周期的对象中,特别是保存到全局变量时,会阻止对短生命周期对象的垃圾回收(从而可能影响程序的性能)。
四:赋值
1:Go支持复合赋值语句,比如:a += 3
2:数值变量支持++递增和--递减语句。
注意:自增和自减是语句,而不是表达式,因此x = i++之类的表达式是错误的;自增和自减只支持后缀形式,不支持前缀。所以,下面的三条语句都是错误的:
b = a++ fmt.Println(a++) ++a
3:元组赋值是另一种形式的赋值语句,它允许同时更新多个变量的值。在赋值之前,赋值语句右边的所有表达式将会先进行求值,然后再统一更新左边对应变量的值。所以可以这样交换两个变量的值:x, y = y, x
4:赋值语句是显式的赋值形式。程序中还有很多地方会发生隐式的赋值行为:函数调用会隐式地将调用参数的值赋值给函数的参数变量,一个返回语句将隐式地将返回操作的值赋值给结果变量,一个复合类型的字面量也会产生赋值行为。例如下面的语句:
medals := []string{"gold", "silver", "bronze"}
隐式地对slice的每个元素进行赋值操作,类似这样写的行为:
medals[0] = "gold" medals[1] = "silver" medals[2] = "bronze"
不管是隐式还是显式地赋值,只有右边的值对于左边的变量是可赋值的,赋值才是允许的。
可赋值性的规则对于不同类型有着不同要求,目前而言,它的规则是简单的:类型必须完全匹配,nil可以赋值给任何指针或引用类型的变量。常量则有更灵活的赋值规则,因为这样可以避免不必要的显式的类型转换。
对于两个值是否可以用==或!=进行相等比较的能力也和可赋值能力有关系:对于任何类型的值的相等比较,第二个值必须是对第一个值类型对应的变量是可赋值的,反之依然。
五:类型
1:变量或表达式的类型定义了对应存储值的属性特征,例如数值在内存的存储大小(或者是元素的bit个数),它们在内部是如何表达的,是否支持一些操作符,以及它们自己关联的方法集等。
2:类型声明语句创建了一个新的类型名称,和现有类型具有相同的底层结构。新命名的类型提供了一个方法,用来分隔不同概念的类型,这样即使它们底层类型相同也是不兼容的。
类型声明的格式是:type 类型名字 底层类型
类型声明语句一般出现在包一级,因此如果新创建的类型名字的首字符大写,则在外部包也可以使用。
3:下面的语句:
type Celsius float64 // 摄氏温度 type Fahrenheit float64 // 华氏温度
声明了两种类型:Celsius和Fahrenheit分别对应不同的温度单位。它们虽然有着相同的底层类型float64,但是它们是不同的数据类型,因此它们不可以被相互比较或混在一个表达式运算。刻意区分类型,可以避免一些像无意中使用不同单位的温度混合计算导致的错误。
4:对于每一个类型T,都有一个对应的类型转换操作T(x),用于将x转为T类型(译注:如果T是指针类型,可能会需要用小括弧包装T,比如(*int)(0))。只有当两个类型的底层基础类型相同时,才允许这种转型操作,或者是两者都是指向相同底层结构的指针类型。这些转换只改变类型而不会影响值本身。
数值类型之间的转型也是允许的,字符串和一些特定类型的slice之间也是可以转换的,这类转换可能改变值的表现。例如,将一个浮点数转为整数将丢弃小数部分,将一个字符串转为[]byte类型的slice将拷贝一个字符串数据的副本。在任何情况下,运行时不会发生转换失败的错误(译注: 错误只会发生在编译阶段)。
下面的转换都是合法的;
var a int = 1 x := Celsius(1) y := Celsius(a) z := float64(a)
5:比较运算符如==或<可以用来比较一个命名类型的变量和另一个有相同类型的变量,或有着相同底层类型的未命名类型的值之间做比较。但是如果两个值有着不同的类型,则不能直接进行比较:
var c Celsius var f Fahrenheit fmt.Println(c == 0) // "true" fmt.Println(f >= 0) // "true" fmt.Println(c == f) // compile error: type mismatch fmt.Println(f >= float64(3)) // compile error: type mismatch fmt.Println(c == Celsius(f)) // "true"!
6:命名类型还可以为该类型的值定义新的行为。这些行为表示为一组关联到该类型的函数集合,称为类型的方法集。后续会详细介绍,这里仅说写简单用法:
func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }
Celsius类型的参数c出现在了函数名的前面,表示声明的是Celsius类型的一个叫名叫String的方法,该方法返回该类型对象的字符串表示。
许多类型都会定义一个String方法,因为当使用fmt包的打印方法时,将会优先使用该类型对应的String方法返回的结果打印:
x := Celsius(1.04) fmt.Println(x) //1.04°C
六:包和文件
1:Go语言中的包和其他语言的库或模块的概念类似,目的都是为了支持模块化、封装、单独编译和代码重用。
一个包的源代码保存在一个或多个以.go为文件后缀名的源文件中,同一个包中的多个源文件中,不能定义同名的变量、函数和类型等。
通常一个包所在目录路径的后缀是包的导入路径;例如包gopl.io/ch1/helloworld对应的目录路径是$GOPATH/src/gopl.io/ch1/helloworld。
2:每个包都对应一个独立的名字空间。例如,在image包中的Decode函数和在unicode/utf16包中的 Decode函数是不同的。要在外部引用该函数,必须显式使用image.Decode或utf16.Decode形式访问。
3:包级别的名字,例如在一个文件声明的类型和常量,在同一个包的其他源文件是可以直接访问的,就好像所有代码都在一个文件一样。
包可以让我们通过控制哪些名字是外部可见的来隐藏内部实现信息。一个简单的规则是:如果一个名字是大写字母开头的,那么该名字是导出的(译注:因为汉字不区分大小写,因此汉字开头的名字是没有导出的)。
4:每个源文件都是以包的声明语句开始,用来指名包的名字。包声明前可以有包注释。一个包通常只有一个源文件有包注释。如果有多个包注释,目前的文档工具会根据源文件名的先后顺序将它们链接为一个包注释。如果包注释很大,通常会放到一个独立的doc.go文件中。
5:在Go语言程序中,每个包都是有一个全局唯一的导入路径。导入语句中类似"gopl.io/ch2/tempconv"的字符串对应包的导入路径。Go语言的规范并没有定义这些字符串的具体含义或包来自哪里,它们是由构建工具来解释的。当使用Go语言自带的go工具箱时,一个导入路径代表一个目录中的一个或多个Go源文件。
除了包的导入路径,每个包还有一个包名,包名一般是短小的名字(并不要求包名是唯一的),包名在包的声明处指定。按照惯例,一个包的名字和包的导入路径的最后一个字段相同,例如gopl.io/ch2/tempconv包的名字一般是tempconv。
导入语句将导入的包绑定到包名,然后通过该包名就可以引用包中导出的全部内容。因此可以tempconv.CToF的形式来访问gopl.io/ch2/tempconv包中的内容。在默认情况下,导入的包绑定到tempconv名字(译注:这包声明语句指定的名字),但是我们也可以绑定到另一个名称,以避免名字冲突。
6:如果导入了一个包,但是又没有使用该包将被当作一个编译错误处理。
7:可以使用golang.org/x/tools/cmd/goimports导入工具,它可以根据需要自动添加或删除导入的包。
8:包的初始化首先是解决包级变量的依赖顺序,然后按照包级变量声明出现的顺序依次初始化:
var a = b + c // a 第三个初始化, 为 3 var b = f() // b 第二个初始化, 为 2, 通过调用 f (依赖c) var c = 1 // c 第一个初始化, 为 1 func f() int { return c + 1 }
如果包中含有多个.go源文件,Go语言的构建工具首先会将.go文件根据文件名排序,然后依次调用编译器编译,如果同一个包中多个源文件中的变量有相互依赖的关系,则初始化顺序也可以是穿插的。
每个文件都可以包含多个init初始化函数。这样的init初始化函数除了不能被调用或引用外,其他行为和普通函数类似。在每个文件中的init初始化函数,在程序开始执行时按照它们声明的顺序被自动调用。
包的初始化过程中,首先初始化所有go文件的变量,然后才是调用init函数。比如,如果包中包含两个go文件:file1和file2,file1和file2的代码分别如下:
//file1.go var a int = getvalue(3) var b int = c + getvalue(2) func init() { fmt.Println("this is p1 init func") } //file2.go func getvalue(arg int) int { fmt.Printf("this is getvalue(%d)\n", arg) return arg } func init() { fmt.Println("this is p2 init func") } func init() { fmt.Println("this is p22 init func") } var c int = getvalue(33) var d int = getvalue(11) + getvalue(111)
根据文件名的排序,首先初始化file1中定义的变量a,该变量需要调用file2中定义的getvalue函数;然后初始化变量b,该变量需要用file2中定义的变量c,因此先初始化file2中的变量c,之后再初始化b;file1中的变量初始化完成之后,然后是初始化file2中的变量d;所有变量初始化完成之后,开始调用文件中的init函数,首先是file1中的init,然后是file2中的两个init,因此,运行结果如下:
this is getvalue(3) this is getvalue(33) this is getvalue(2) this is getvalue(11) this is getvalue(111) this is p1 init func this is p2 init func this is p22 init func
10:每个包在解决依赖的前提下,以导入声明的顺序初始化,每个包只会被初始化一次。因此,如果一个p包导入了q包,那么在p包初始化的时候q包必然已经初始化过了。初始化工作是自下而上进行的,main包最后被初始化。以这种方式,可以确保在main函数执行之前,所有依然的包都已经完成初始化工作了。
七:作用域
1:声明语句的作用域是指源代码中可以有效使用这个名字的范围。
不要将作用域和生命周期混为一谈。声明语句的作用域对应的是一个源代码的文本区域;它是一个编译时的属性。一个变量的生命周期是指程序运行时变量存在的有效时间段,在此时间区域内它可以被程序的其他部分引用;是一个运行时的概念。
2:语法块是由花括弧所包含的一系列语句,语法块内部声明的名字是无法被外部语法块访问的。
3:对于内置的类型、函数和常量,比如int、len和true等是在全局作用域的,因此可以在整个程序中直接使用;任何在在函数外部(也就是包级语法域)声明的名字可以在同一个包的任何源文件中访问的;对于导入的包,例如tempconv导入的fmt包,则是对应源文件级的作用域,因此只能在当前的文件中访问导入的fmt包,当前包的其它源文件无法访问在当前源文件导入的包;还有许多声明语句,则是局部作用域的,它只能在函数内部(甚至只能是局部的某些部分)访问;控制流标号,就是break、continue或goto语句后面跟着的那种标号,则是函数级的作用域。
4:一个程序可能包含多个同名的声明,只要它们在不同的词法域就没有关系。例如,可以声明一个局部变量,和包级的变量同名。
当编译器遇到一个名字引用时,它首先从最内层的词法域向全局的作用域查找。如果查找失败,则报告“未声明的名字”这样的错误。如果该名字在内部和外部的块分别声明过,则内部块的声明首先被找到。在这种情况下,内部声明屏蔽了外部同名的声明。
5:有许多语法块是if或for等控制流语句构造的:
for i := 0; i < len(x); i++ { x := x[i] if x != '!' { x := x + 'A' - 'a' fmt.Printf("%c", x) } }
正如上面例子所示,并不是所有的词法域都显式地对应到由花括弧包含的语句;还有一些隐含的规则。上面的for语句创建了两个词法域:花括弧包含的是显式的for循环体部分词法域,另外一个隐式的部分则是循环的初始化部分,比如用于迭代变量i的初始化。隐式的词法域部分的作用域包含条件测试部分和循环后的迭代部分( i++ ),当然也包含循环体词法域。
和for循环类似,if和switch语句也会在条件部分创建隐式词法域,还有它们对应的执行体词法域。下面的if-else测试链演示了x和y的有效作用域范围:
if x := f(); x == 0 { fmt.Println(x) } else if y := g(x); x == y { fmt.Println(x, y) } else { fmt.Println(x, y) } fmt.Println(x, y) // compile error: x and y are not visible here
第二个if语句嵌套在第一个内部,因此第一个if语句条件初始化词法域声明的变量在第二个if中也可以访问。switch语句的每个分支也有类似的词法域规则:条件部分为一个隐式词法域,然后是每个分支的词法域。
6:在包级别,声明的顺序并不会影响作用域范围,因此一个先声明的可以引用它自身或者是引用后面的一个声明,但是如果一个变量或常量递归引用了自身,则会产生编译错误。
要特别注意短变量声明语句的作用域范围,考虑下面的程序:
var cwd string func init() { cwd, err := os.Getwd() // compile error: unused: cwd if err != nil { log.Fatalf("os.Getwd failed: %v", err) } }
虽然cwd在外部已经声明过,但是 := 语句还是将cwd和err重新声明为新的局部变量。因为内部声明的cwd将屏蔽外部的声明,因此上面的代码并不会正确更新包级声明的cwd变量。
有许多方式可以避免出现类似潜在的问题。最直接的方法是通过单独声明err变量,来避免使用 := 的简短声明方式:
var cwd string func init() { var err error cwd, err = os.Getwd() if err != nil { log.Fatalf("os.Getwd failed: %v", err) } }
http://docs.ruanjiadeng.com/gopl-zh/ch2/ch2.html