《go语言圣经》之程序结构
说明:本内容整理自《go语言圣经》
1.命名
go语言的命名和其他语言差不多,一个名字必须以一个字母或者下划线开头,后面可以跟任意的数字和字母,命名是区分大小写的。
关于go语言命名的说明:
- 不能以关键字命名,go语言的关键字有25个,关键字有: breakdefault,func,interface,select
case,defer,go,map,struct,chan,else
goto,package,switch,const,fallthrough if,range,type,continue,for,import,return,var - 不能以预定义的名字命名,比如int,true等
- 如果一个名字在函数内部定义,它在函数内部有效
- 如果在函数外部定义,在当前包的所有文件中有效
- 如果需要其他包能够访问,不管是变量还是函数,以大写开头
- 包名一般小写
2.声明
声明语句定义了程序的各种实例以及它的属性,在go语言中有四种声明类型:
- var:变量
- const:常量
- type:类型
- func:函数
3.变量
3.1 var
var声明语句可以创建一个指定类型的变量,然后给变量命名和赋值,一般的语法如下:
var 变量名 变量类型 = 表达式
变量类型和表达式可以省略其中一个,也可以在一个声明语句中声明多个变量,如下格式:
var i,j,k int
var b,f,s = true,2.3,"one"
一组变量也可以通过调用一个函数,由函数返回值赋值,如下:
var f,err = os.Open(name)
在函数内部有一种,称为简短变量声明的形式,如下:
f,err := os.Open(name)
关于var声明和简短声明的主要用法区别在于:
- var适用于需要显示指定变量类型,或者初始化值为零值得情况
- 简短声明主要用于初始化值不为零值得情况
- “:=”是一个变量声明语句,而“=”是一个赋值语句
- 简短变量声明可以用函数返回值来声明和初始化变量
简短变量声明必须保证至少有一个变量是之前未声明的变量,下面的代码将不能编译通过:
f,err := os.Open(infile)
f,err := os.Create(outfile)
3.2 指针
一个变量对应于内存中的一个值,但是有时有些值并没有变量名与之对应,比如x[i],x[1]等,这时指针可以派上用场。
一个指针的值对应一个变量的地址,通过指针我们可以直接更新变量的值而不必知道其名字。
如果我们有声明语句“var x int”,那么&x表示取变量x的地址,也即表示指向x的一个指针,指针对应的数据类型是*int,如果令 p := &x,那么我们可以说“p指针指向变量x”或者说“p指针保存了变量x的内存地址”,如下代码展示一个简单的指针应用:
x := 1
p := &x
fmt.Println(*p)
任何类型的指针的零值都是nil,如果 p != nil为真,那么说明这个指针指向的变量为一个有效地变量。
一般说来,如果一个函数调用完成,那么其内的局部变量将会被释放,但是如果一个函数返回一个指针,那么这个函数内与指针相联系的变量将不会被释放,如下代码中,变量v依然有效:
var p=f()
func f() *int {
v := 1
return &v
}
如果将指针作为参数调用函数,那么可以在函数中通过该指针更新变量的值。
每一次对一个变量取地址,或者复制指针,我们都是为原变量创建了新的别名。例如,*p就是变量v的别名。
3.3 new函数
另一种创建变量的方法是使用new函数,表达式new(T)表示创建一个T类型的匿名变量,初始化为T类型的零值。如下代码:
p := new(int)
fmt.Println(*p)
*p = 2
fmt.Println(*p)
3.4 变量的生命周期
变量的生命周期指的是程序运行开始到程序运行结束,如果变量在函数内,那么变量的生命周期就是函数调用开始到函数调用结束。
go语言的垃圾回收器是怎么知道一个变量何时可以被回收?大体上的思路就是:从包级变量和函数内的局部变量开始沿着指针或引用的访问路径遍历,是否可以找到该变量,如果可以找到,说明该变量还在使用,如果没有,则可以回收。
对于一个变量是被分配在堆上还是在栈上这是由编译器决定的,一个很重要的例子就是:当一个局部变量从函数中逃逸的时候,编译器会把这个变量分配到堆里,其他局部变量分配到栈里,逃逸指的是,虽然这个函数调用完成,但是这个函数内的某个或者某些局部变量被外部变量所使用着。
4.赋值
赋值就是更新一个变量的值,最简单的赋值就是“变量 = 值”这种表达式。
注意:go语言中的++递增和–递减是语句,而不是表达式,像“x=i++”是错误的。
4.1元组的赋值
元组赋值允许同时更新多个变量的值,赋值之前,赋值符号右边的表达式先会求值,然后在统一赋值给左边的变量,例如如下的代码:
x,y = y,x
a[i],a[j] = a[j],a[i]
当然,使用比较多的是函数返回值的赋值,如下代码:
f,err := os.Open("foo.txt")
和变量声明一样,我们可以利用“_”来丢弃我们不需要的变量,如下面代码所示:
_,err = io.Copy(dst,src)
4.2可赋值性
一句话可以总结可赋值性,即只有赋值符号右边的变量类型等于左边的变量类型赋值才算成功。
5.类型
变量或表达式的类型定义了对应存储的特性,包括它们有哪些属性,在内部是如何表达的,是否支持一些操作,以及它们自己关联的一些方法集。
一个类型声明语句创建了一个新的类型,类型声明的语法结构是:
type 类型名 底层类型名
例如:
type Celsius float64
type Fahrenheit float64
虽然Celsius和Fahrenheit底层数据结构都是float64,但是它们是不同的数据类型。
每个类型都对应一个T(x),用于将x转换为T。如果T是指针类型的,可能需要用小括号包装T,比如(*int)(0)。
6.包和文件
go语言的包和其他语言的库或模块差不多,一个包的源码保存在一个或多个.go的文件中,通常来说,一个包在import当中的路径对应的目录路径是 GOPATH/src/包名或者 GOROOT/src/包名。例如:
import("pgk/test") 对应的目录为
$GOROOT/src/pkg/test或者$/GOPATH/src/pkg/test
6.1包的初始化
包的初始化首先是解决包级变量的依赖关系,然后按照包级变量的出现顺序依次初始化。例如:
var a=b+c //a 第三个初始化,依赖于b和C
var b=f() //b 第二个初始化,依赖于f()
var c=1 //c 第一个初始化
func f() int {return c+1}
如果包中包含多个.go的源文件,它们将按照编译器的顺序进行初始化,go语言的构建工具首先会将.go文件按照文件名进行排序,然后依次调用编辑器。
对于在包级别的变量,如果有初始化表达式,那么对表达式进行初始化,如果有一些没有初始化表达式的,比如表格初始化,那么可以用init()函数来实现。这个函数除了不能被调用和引用外,其他没什么区别,init()函数如下:
func init() {}
7.作用域
一个声明语句的作用域指的是源代码中可以有效访问这个名字的范围。
关于作用域和生命周期的区别:
- 声明语句的作用域对应是源代码的文本区域,它是一个编译时的属性
- 变量的生命周期指的是程序运行时变量存在的有效时间段,在此区域内它可以被程序的其他部分引用,是一个运行时概念。
语法块是由花括号所包含的一系列语句,语法块内部声明的变量是无法被外部访问的。
声明语句对应的词法域决定了作用域范围的大小,具体来说:
- 对于一个内置的类型,函数和变量,如果它们是在全局作用域的,可以在整个程序中使用
- 在包级作用域声明的名字可以再同一包内的任何源文件进行访问
- 对于导入的包,只能在当前文件中进行访问,而当前文件所属的包内的其他文件时无法访问的
- 如果一个名字在内部和外部同时声明了(比如,在全局作用域和一个函数内同时声明了一个变量name),那么内部块的声明会被先找到,外部块的名字会被屏蔽。