Go语言与数据库开发:01-02

接下来,开始了解go语言的程序结构,基础要打牢。

Go语言和其他编程语言一样,一个大的程序是由很多小的基础构件组成的。变量保存值,简
单的加法和减法运算被组合成较复杂的表达式。基础类型被聚合为数组或结构体等更复杂的
数据结构。然后使用if和for之类的控制语句来组织和控制表达式的执行流程。然后多个语句被
组织到一个个函数中,以便代码的隔离和复用。函数以源文件和包的方式被组织。


. 关于命名:


在Go中是区分大小写的;关键字不能用于自定义名字;
Go语言的风格是尽量使用短小的名字,对于局部变量尤其是这样;你会经常看到i之类的短名字,
而不是冗长的theLoopIndex命名。通常来说,如果一个名字的作用域比较大,生命周期也比较长,
那么用长的名字将会更有意义。

在习惯上,Go语言程序员推荐使用 驼峰式 命名,当名字有几个单词组成的时优先使用大小写
分隔,而不是优先用下划线分隔。因此,在标准库有QuoteRuneToASCII和parseRequestLine
这样的函数命名,但是一般不会用quote_rune_to_ASCII和parse_request_line这样的命名。


. 声明:


声明语句定义了程序的各种实体对象以及部分或全部的属性。

Go语言主要有四种类型的声明语句:
var 变量
const 常量
type 类型
func 函数实体对象的声明

一个Go语言编写的程序对应一个或多个以.go为文件后缀名的源文件中。
每个源文件以包的声明语句开始,说明该源文件是属于哪个包。

一个声明的例子:
// Boiling prints the boiling point of water.
package main
import "fmt"
const boilingF = 212.0
func main() {
var f = boilingF
var c = (f - 32) * 5 / 9
fmt.Printf("boiling point = %g°F or %g°Cn", f, c)
// Output:
// boiling point = 212°F or 100°C
}

其中常量boilingF是在包一级范围声明语句声明的,然后f和c两个变量是在main函数内部声明
的声明语句声明的。在包一级声明语句声明的名字可在整个包对应的每个源文件中访问,而
不是仅仅在其声明语句所在的源文件中访问。相比之下,局部声明的名字就只能在函数内部
很小的范围被访问。


.变量


var 变量名字 类型 = 表达式

其中“类型”或“= 表达式”两个部分可以省略其中的一个。
如果省略的是类型信息,那么将根据初始化表达式来推导变量的类型信息。
如果初始化表达式被省略,那么将用零值初始化该变量。

不同类型变量对应的零值是不同的:
数值类型变量对应的零值是0
布尔类型变量对应的零值是false
字符串类型对应的零值是空字符串
接口或引用类型(包括slice、map、chan和函数)变量对应的零值是nil
数组或结构体等聚合类型对应的零值是每个元素或字段都是对应该类型的零值

零值初始化机制可以确保每个声明的变量总是有一个良好定义的值,因此在Go语言中不存在未初始化的变量。

可以在一个声明语句中同时声明一组变量,或用一组初始化表达式声明并初始化一组变量。
如果省略每个变量的类型,将可以声明多个类型不同的变量(类型由初始化表达式推导):
var i, j, k int // int, int, int
var b, f, s = true, 2.3, "four" // bool, float64, string

初始化表达式可以是字面量或任意的表达式。
在包级别声明的变量会在main入口函数执行前完成初始化,
局部变量将在声明语句被执行到的时候完成初始化。

一组变量也可以通过调用一个函数,由函数返回的多个返回值初始化:
var f, err = os.Open(name) // os.Open returns a file and an error


. 简短变量


在函数内部,有一种称为简短变量声明语句的形式可用于声明和初始化局部变量。

以“名字:= 表达式”形式声明变量,变量的类型根据表达式来自动推导。

例如:
anim := gif.GIF{LoopCount: nframes}
freq := rand.Float64() * 3.0
t := 0.0

因为简洁和灵活的特点,简短变量声明被广泛用于大部分的局部变量的声明和初始化

简短变量声明语句也可以用来声明和初始化一组变量:
i, j := 0, 1
但是这种同时声明多个变量的方式应该限制只在可以提高代码可读性的地方使用,比如for语
句的循环的初始化语句部分。

请记住“:=”是一个变量声明语句,而“=‘是一个变量赋值操作。

简短变量声明左边的变量可能并不是全部都是刚刚声明的。如果有一些已经在相同的词法域声
明过了,那么简短变量声明语句对这些已经声明过的变量就只有赋值行为了。
也就是说,已经声明过了以后,后续对简短变量就只能有赋值行为了。

简短变量声明语句只有对已经在同级词法域声明过的变量才和赋值操作语句等价,如果变量
是在外部词法域声明的,那么简短变量声明语句将会在当前词法域重新声明一个新的变量。


. 指针


一个变量对应一个保存了变量对应类型值的内存空间。

一个指针的值是另一个变量的地址。一个指针对应变量在内存中的存储位置。并不是每一个
值都会有一个内存地址,但是对于每一个变量必然有对应的内存地址。通过指针,我们可以
直接读或更新对应变量的值,而不需要知道该变量的名字(如果变量有名字的话)。

如果用“var x int”声明语句声明一个x变量,那么&x表达式(取x变量的内存地址)将产生一个
指向该整数变量的指针,指针对应的数据类型是 *int ,指针被称之为“指向int类型的指针”。
如果指针名字为p,那么可以说“p指针指向变量x”,或者说“p指针保存了x变量的内存地址”。

同时,p 表达式对应p指针指向的变量的值。一般 p 表达式读取指针指向的变量的值,这里
为int类型的值,同时因为 *p 对应一个变量,所以该表达式也可以出现在赋值语句的左边,表
示更新指针所指向的变量的值。

例如:
x := 1
p := &x // p, of type *int, points to x
fmt.Println(*p) // "1"
*p = 2 // equivalent to x = 2
fmt.Println(x) // "2"

变量有时候被称为可寻址的值。即使变量由表达式临时生成,那么表达式也必须能接受 & 取地址操作。
任何类型的指针的零值都是nil。如果 p != nil 测试为真,那么p是指向某个有效变量。指针之间也
是可以进行相等测试的,只有当它们指向同一个变量或全部是nil时才相等。

var x, y int
fmt.Println(&x == &x, &x == &y, &x == nil) // "true false false"

在Go语言中,返回函数中局部变量的地址也是安全的。例如下面的代码,调用f函数时创建局
部变量v,在局部变量地址被返回之后依然有效,因为指针p依然引用这个变量。

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

因为指针包含了一个变量的地址,因此如果将指针作为参数调用函数,那将可以在函数中通
过该指针来更新变量的值。
例如下面这个例子就是通过指针来更新变量的值,然后返回更新
后的值,可用在一个表达式中

func incr(p *int) int {
*p++ // 非常重要:只是增加p指向的变量的值,并不改变p指针!!!
return *p
}

每次我们对一个变量取地址,或者复制指针,我们都是为原变量创建了新的别名。


如, *p 就是 变量v的别名。指针特别有价值的地方在于我们可以不用名字而访问一个变
量,但是这是一把双刃剑:要找到一个变量的所有访问者并不容易,我们必须知道变量全部
的别名(译注:这是Go语言的垃圾回收器所做的工作)。

不仅仅是指针会创建别名,很多其他引用类型也会创建别名,例如slice、map和chan,甚至结
构体、数组和接口都会创建所引用变量的别名。


. New函数


另一个创建变量的方法是调用用内建的new函数。表达式new(T)将创建一个T类型的匿名变
量,初始化为T类型的零值,然后返回变量地址,返回的指针类型为 *T 。

p := new(int) // p, *int 类型, 指向匿名的 int 变量
fmt.Println(*p) // "0"
*p = 2 // 设置 int 匿名变量的值为 2
fmt.Println(*p) // "2"

用new创建变量和普通变量声明语句方式创建变量没有什么区别,除了不需要声明一个临时变
量的名字外,我们还可以在表达式中使用new(T)。

由于new只是一个预定义的函数,它并不是一个关键字,因此我们可以将new名字重新定义为
别的类型。


. 变量的生命周期及GC


变量的生命周期指的是在程序运行期间变量有效存在的时间间隔。

变量的生命周期指的是在程序运行期间变量有效存在的时间间隔。对于在包一级声明的变量
来说,它们的生命周期和整个程序的运行周期是一致的。而相比之下,在局部变量的声明周
期则是动态的:从每次创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后
变量的存储空间可能被回收。函数的参数变量和返回值变量都是局部变量。它们在函数每次
被调用的时候创建。

那么垃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语言的自动垃圾收集器对编写正确的代码是一个巨大的帮助,但也并不是说你完全不用考
虑内存了。你虽然不需要显式地分配和释放内存,但是要编写高效的程序你依然需要了解变
量的生命周期。例如,如果将指向短生命周期对象的指针保存到具有长生命周期的对象中,
特别是保存到全局变量时,会阻止对短生命周期对象的垃圾回收(从而可能影响程序的性
能)。


. 赋值


使用赋值语句可以更新一个变量的值。

例子:
x = 1 // 命名变量的赋值
*p = true // 通过指针间接赋值
person.name = "bob" // 结构体字段赋值
count[x] = count[x] * scale // 数组、slice或map的元素赋值

另外,还有如下简洁的书写方式:
count[x] *= scale

v := 1
v++ // 等价方式 v = v + 1;v 变成 2
v-- // 等价方式 v = v - 1;v 变成 1

元组赋值:
元组赋值是另一种形式的赋值语句,它允许同时更新多个变量的值。在赋值之前,赋值语句
右边的所有表达式将会先进行求值,然后再统一更新左边对应变量的值。这对于处理有些同
时出现在元组赋值语句左右两边的变量很有帮助,例如我们可以这样交换两个变量的值:

x, y = y, x
a[i], a[j] = a[j], a[i]

元组赋值也可以使一系列琐碎赋值更加紧凑:
i, j, k = 2, 3, 5

但如果表达式太复杂的话,应该尽量避免过度使用元组赋值;因为每个变量单独赋值语句的
写法可读性会更好。

有些表达式会产生多个值,比如调用一个有多个返回值的函数。当这样一个函数调用出现在
元组赋值右边的表达式中时(注:右边不能再有其它表达式),左边变量的数目必须和右
边一致。
f, err = os.Open("foo.txt") // function call returns two values

可赋值性:
赋值语句是显式的赋值形式,但是程序中还有很多地方会发生隐式的赋值行为:函数调用会
隐式地将调用参数的值赋值给函数的参数变量,一个返回语句将隐式地将返回操作的值赋值
给结果变量,一个复合类型的字面量也会产生赋值行为。

例如:

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

medals[0] = "gold"
medals[1] = "silver"
medals[2] = "bronze"

只有右边的值对于左边的变量是可赋值的,赋值语句才是允许的。


. 类型


一个类型声明语句创建了一个新的类型名称,和现有类型具有相同的底层结构。新命名的类
型提供了一个方法,用来分隔不同概念的类型,这样即使它们底层类型相同也是不兼容的。

type 类型名字 底层类型

类型声明语句一般出现在包一级,因此如果新创建的类型名字的 首字符大写 ,则在外部包也可以使用。

例如:
package tempconv
import "fmt"
type Celsius float64 // 摄氏温度
type Fahrenheit float64 // 华氏温度
const (
AbsoluteZeroC Celsius = -273.15 // 绝对零度
FreezingC Celsius = 0 // 结冰点温度
BoilingC Celsius = 100 // 沸水温度
)
func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }
func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }

在这个包声明了两种类型:Celsius和Fahrenheit分别对应不同的温度单位。它们虽然有
着相同的底层类型float64,但是它们是不同的数据类型,因此它们不可以被相互比较或混在
一个表达式运算。

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

数值类型之间的转型也是允许的,并且在字符串和一些特定类型的slice之间也是可以转换的;


. 包和文件


Go语言中的包和其他语言的库或模块的概念类似,目的都是为了支持模块化、封装、单独编
译和代码重用。一个包的源代码保存在一个或多个以.go为文件后缀名的源文件中,通常一个
包所在目录路径的后缀是包的导入路径;例如包gopl.io/ch1/helloworld对应的目录路径是
$GOPATH/src/gopl.io/ch1/helloworld

每个包都对应一个独立的名字空间

包还可以让我们通过控制哪些名字是外部可见的来隐藏内部实现信息。在Go语言中,一个简
单的规则是:如果一个名字是大写字母开头的,那么该名字是导出的.

包级别的名字,例如在一个文件声明的类型和常量,在同一个包的其他源文件也是可以直接
访问的,就好像所有代码都在一个文件一样.

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

导入包:
在Go语言程序中,每个包都是有一个全局唯一的导入路径。
Go语言的规范并没有定义这些字符串的具体含义或包来自哪里,它们是由构建工具来解释的。
当使用Go语言自带的go工具箱时,一个导入路径代表一个目录中的一个或多个Go源文件。

除了包的导入路径,每个包还有一个包名,包名一般是短小的名字(并不要求包名是唯一
的),包名在包的声明处指定。按照惯例,一个包的名字和包的导入路径的最后一个字段相
同,例如gopl.io/ch2/tempconv包的名字一般是tempconv。

要使用gopl.io/ch2/tempconv包,需要先导入:
import (
"gopl.io/ch2/tempconv"
)

导入语句将导入的包绑定到一个短小的名字,然后通过该短小的名字就可以引用包中导出的
全部内容。
导入声明将允许我们以tempconv.CToF的形式来访问gopl.io/ch2/tempconv包中的内容。
但是我们也可以绑定到另一个名称,以避免名字冲突。

如果导入了一个包,但是又没有使用该包将被当作一个编译错误处理。这种强制规则可以有
效减少不必要的依赖,虽然在调试期间可能会让人讨厌。

包的初始化:

包的初始化首先是解决包级变量的依赖顺序,然后安照包级变量声明出现的顺序依次初始化:

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初始化函数
func init() { / ... / }
这样的init初始化函数除了不能被调用或引用外,其他行为和普通函数类似。在每个文件中的
init初始化函数,在程序开始执行时按照它们声明的顺序被自动调用。

每个包在解决依赖的前提下,以导入声明的顺序初始化,每个包只会被初始化一次。因此,
如果一个p包导入了q包,那么在p包初始化的时候可以认为q包必然已经初始化过了。初始化
工作是自下而上进行的,main包最后被初始化。以这种方式,可以确保在main函数执行之
前,所有依然的包都已经完成初始化工作了。


. 作用域


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

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

语法块是由花括弧所包含的一系列语句,就像函数体或循环体花括弧对应的语法块那样。语
法块内部声明的名字是无法被外部语法块访问的。

语法决定了内部声明的名字的作用域范围。

语法块可以包含其他类似组批量声明等没有用花括弧包含的代码,
我们称之为语法块。有一个语法块为整个源代码,称为全局语法块;然后是每个包的包语法
决;每个for、if和switch语句的语法决;每个switch或select的分支也有独立的语法决;当然也
包括显式书写的语法块(花括弧包含的语句)。

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

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

一个程序可能包含多个同名的声明,只要它们在不同的词法域就没有关系。

当编译器遇到一个名字引用时,如果它看起来像一个声明,它首先从最内层的词法域向全局
的作用域查找。如果查找失败,则报告“未声明的名字”这样的错误。如果该名字在内部和外部
的块分别声明过,则内部块的声明首先被找到。在这种情况下,内部声明屏蔽了外部同名的
声明,让外部的声明的名字无法被访问。

在包级别,声明的顺序并不会影响作用域范围,因此一个先声明的可以引用它自身或者是引
用后面的一个声明,这可以让我们定义一些相互嵌套或递归的类型或函数。

和for循环类似,if和switch语句也会在条件部分创建隐式词法域,还有它们对应的执行体词法域:
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语句的每个分支也有类似的词法域规则:条件部分为一个隐式词法域,然
后每个是每个分支的词法域。

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

if f, err := os.Open(fname); err != nil { // compile error: unused: f
return err
}
f.ReadByte() // compile error: undefined f
f.Close() // compile error: undefined f

变量f的作用域只有在if语句内,因此后面的语句将无法引入它,这将导致编译错误。

通常需要在if之前声明变量,这样可以确保后面的语句依然可以访问变量:
f, err := os.Open(fname)
if err != nil {
return err
}
f.ReadByte()
f.Close()

你可能会考虑通过将ReadByte和Close移动到if的else块来解决这个问题:
if f, err := os.Open(fname); err != nil {
return err
} else {
// f and err are visible here too
f.ReadByte()
f.Close()
}

但这不是Go语言推荐的做法,Go语言的习惯是在if中处理错误然后直接返回,这样可以确保
正常执行的语句不需要代码缩进。

上一篇:Linux裸设备管理详解(原创)


下一篇:OpenStack —— 镜像服务Glance(三)