Skip to content

go语言程序结构

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

命名

Go语言中的函数名、变量名、常量名、类型名、语句标号和包名等所有的命名,都遵循一个 简单的命名规则:一个名字必须以一个字母(Unicode字母)或下划线开=头,后面可以跟任意 数量的字母、数字或下划线。大写字母和小写字母是不同的:heapSort和Heapsort是两个不同的名字。

关键字

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

预定义的名字

  • 内建常量: true false iota nil
  • 内建类型: int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 uintptr float32 float64 complex128 complex64 bool byte rune string error
  • 内建函数: make len cap new append copy close delete complex real imag panic recover

这些内部预先定义的名字并不是关键字,你可以再定义中重新使用它们。在一些特殊的场景 中重新定义它们也是有意义的,但是也要注意避免过度而引起语义混乱。

可见性

名字的开头字母的大小写决定了名字在包外的可见性。如果一个名字是大写字母开头的(译注:必须是在函数外部定义的包级名字;包级函 数名本身也是包级名字),那么它将是导出的,也就是说可以被外部的包访问,例如fmt包的 Printf函数就是导出的,可以在fmt包外部访问。包本身的名字一般总是用小写字母。

声明

声明语句定义了程序的各种实体对象以及部分或全部的属性。Go语言主要有四种类型的声明 语句:var、const、type和func,分别对应变量、常量、类型和函数实体对象的声明。。每个源文件以包的声 明语句开始,说明该源文件是属于哪个包。包声明语句之后是import语句导入依赖的其它包, 然后是包一级的类型、变量、常量、函数的声明语句,包一级的各种类型的声明语句的顺序 无关紧要(译注:函数内部的名字则必须先声明之后才能使用)

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°C\n", f, c) 
    // Output: 
    // boiling point = 212°F or 100°C 
}

其中常量boilingF是在包一级范围声明语句声明的,然后f和c两个变量是在main函数内部声明 的声明语句声明的。在包一级声明语句声明的名字可在整个包对应的每个源文件中访问,而 不是仅仅在其声明语句所在的源文件中访问。相比之下,局部声明的名字就只能在函数内部 很小的范围被访问。 一个函数的声明由一个函数名字、参数列表(由函数的调用者提供参数变量的具体值)、一 个可选的返回值列表和包含函数定义的函数体组成。如果函数没有返回值,那么返回值列表 是省略的。执行函数从函数的第一个语句开始,依次顺序执行直到遇到renturn返回语句,如 果没有返回语句则是执行到函数末尾,然后返回到函数调用者。

变量

var声明语句可以创建一个特定类型的变量,然后给变量附加一个名字,并且设置变量的初始 值。变量声明的一般语法如下: var 变量名字 类型 = 表达式

其中“类型”或“= 表达式”两个部分可以省略其中的一个。如果省略的是类型信息,那么将根据 初始化表达式来推导变量的类型信息。如果初始化表达式被省略,那么将用零值初始化该变 量。 数值类型变量对应的零值是0,布尔类型变量对应的零值是false,字符串类型对应的零 值是空字符串,接口或引用类型(包括slice、map、chan和函数)变量对应的零值是nil。数 组或结构体等聚合类型对应的零值是每个元素或字段都是对应该类型的零值。

简短变量声明

在函数内部,有一种称为简短变量声明语句的形式可用于声明和初始化局部变量。它以“名字 := 表达式”形式声明变量,变量的类型根据表达式来自动推导。下面是 简短变量声明语句:

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

这里有一个比较微妙的地方:简短变量声明左边的变量可能并不是全部都是刚刚声明的。如 果有一些已经在相同的词法域声明过了,那么简短变量声明语句对这些已经声明过的变量就只有赋值行为了。 简短变量声明语句只有对已经在同级词法域声明过的变量才和赋值操作语句等价,如果变量 是在外部词法域声明的,那么简短变量声明语句将会在当前词法域重新声明一个新的变量。

指针

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

如果用“var x int”声明语句声明一个x变量,那么&x表达式(取x变量的内存地址)将产生一个 指向该整数变量的指针,指针对应的数据类型是 *int ,指针被称之为“指向int类型的指针”。 如果指针名字为p,那么可以说“p指针指向变量x”,或者说“p指针保存了x变量的内存地址”。 同时 *p 表达式对应p指针指向的变量的值。一般 *p 表达式读取指针指向的变量的值,这里 为int类型的值,同时因为 *p 对应一个变量,所以该表达式也可以出现在赋值语句的左边,表 示更新指针所指向的变量的值。

go
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时才相等。

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

指针是实现标准库中flag包的关键技术,它使用命令行参数来设置对应变量的值,而这些对应 命令行标志参数的变量可能会零散分布在整个程序中。为了说明这一点,在早些的echo版本 中,就包含了两个可选的命令行参数: -n 用于忽略行尾的换行符, -s sep 用于指定分隔字 符(默认是空格)。

new函数

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

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

用new创建变量和普通变量声明语句方式创建变量没有什么区别,除了不需要声明一个临时变 量的名字外,我们还可以在表达式中使用new(T)。换言之,new函数类似是一种语法糖,而 不是一个新的基础概念。下面的两个newInt函数有着相同的行为:

go
func newInt() *int { 
    return new(int) 
}
func newInt() *int { 
    var dummy int 
    return &dummy 
}

变量的生命周期

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

那么Go语言的自动圾收集器是如何知道一个变量是何时可以被回收的呢?这里我们可以避开 完整的技术细节,基本的实现思路是,从每个包级的变量和每个当前运行函数的每一个局部 变量开始,通过指针或引用的访问路径遍历,是否可以找到该变量。如果不存在这样的访问 路径,那么说明该变量是不可达的,也就是说它是否存在并不会影响程序后续的计算结果。 因为一个变量的有效周期只取决于是否可达,因此一个循环迭代内部的局部变量的生命周期 可能超出其局部作用域。同时,局部变量可能在函数返回之后依然存在。编译器会自动选择在栈上还是在堆上分配局部变量的存储空间,但可能令人惊讶的是,这个 选择并不是由用var还是new声明变量的方式决定的。

赋值

使用赋值语句可以更新一个变量的值,最简单的赋值语句是将要被赋值的变量放在=的左边, 新值的表达式放在=的右边。特定的二元算术运算符和赋值语句的复合操作有一个简洁形式:count[x] *= scale,数值变量也可以支持 ++ 递增和 -- 递减语句

元组赋值 元组赋值是另一种形式的赋值语句,它允许同时更新多个变量的值。在赋值之前,赋值语句 右边的所有表达式将会先进行求值,然后再统一更新左边对应变量的值

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

有些表达式会产生多个值,比如调用一个有多个返回值的函数。当这样一个函数调用出现在 元组赋值右边的表达式中时(译注:右边不能再有其它表达式),左边变量的数目必须和右 边一致。

go
f, err = os.Open("foo.txt") // function call returns two value

类型

一个类型声明语句创建了一个新的类型名称,和现有类型具有相同的底层结构。新命名的类型提供了一个方法,用来分隔不同概念的类型,这样即使它们底层类型相同也是不兼容的。类型声明语句一般出现在包一级,因此如果新创建的类型名字的首字符大写,则在包外部也可以使用。

go
type 类型名字 底层类型
go
// Package tempconv performs Celsius and Fahrenheit temperature computations.
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,但是它们是不同的数据类型,因此它们不可以被相互比较或混在一个表达式运算。刻意区分类型,可以避免一些像无意中使用不同单位的温度混合计算导致的错误;因此需要一个类似Celsius(t)或Fahrenheit(t)形式的显式转型操作才能将float64转为对应的类型。Celsius(t)和Fahrenheit(t)是类型转换操作,它们并不是函数调用。类型转换不会改变值本身,但是会使它们的语义发生变化。另一方面,CToF和FToC两个函数则是对不同温度单位下的温度进行换算,它们会返回不同的值。

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

包与文件

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

每个包都对应一个独立的名字空间。包还可以让我们通过控制哪些名字是外部可见的来隐藏内部实现信息。在Go语言中,一个简单的规则是:如果一个名字是大写字母开头的,那么该名字是导出的(译注:因为汉字不区分大小写,因此汉字开头的名字是没有导出的)。

包的初始化

包的初始化首先是解决包级变量的依赖顺序,然后按照包级变量声明出现的顺序依次初始化。如果包中含有多个.go源文件,它们将按照发给编译器的顺序进行初始化,Go语言的构建工具首先会将.go文件根据文件名排序,然后依次调用编译器编译。

对于在包级别声明的变量,如果有初始化表达式则用表达式初始化,还有一些没有初始化表达式的,例如某些表格数据初始化并不是一个简单的赋值过程。在这种情况下,我们可以用一个特殊的init初始化函数来简化初始化工作。每个文件都可以包含多个init初始化函数

go
func init() { /* ... */ }

这样的init初始化函数除了不能被调用或引用外,其他行为和普通函数类似。在每个文件中的init初始化函数,在程序开始执行时按照它们声明的顺序被自动调用。

初始化工作是自下而上进行的,main包最后被初始化。以这种方式,可以确保在main函数执行之前,所有依赖的包都已经完成初始化工作了。

作用域

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

Released under the MIT License.