程序结构
一、名称
Go中函数、变量等名称遵循一个简单的规则,即开头是一个字母或下划线,后面可以跟任意数量的字符、数字和下划线,并区分大小写。
Go中有25个关键字,只能用在语法允许的地方,不能作为名称使用,关键字如下:
break | defaullt | 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、itoa、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
二、声明
声明是给一个程序实体命名,并设定其部分或全部属性,共有四种主要的声明:变量(var)、常量(const)、类型(type)和函数(func)。
package main
import "fmt"
type temp struct{} //声明类型
const boilingF = 212.0 //声明常量
func main(){ //声明函数
var f = boilingF //声明变量
var c = (f - 32) * 5 / 9 //声明变量
fmt.Printf("boiling point = %g℉ or %g℃\n", f, c) //输出boiling point = 212℉ or 100℃
}
三、变量
var声明创建一个具体类型的变量,然后给它附加一个名字,设置初始值,形如:
var name type = expression
类型和表达式部分可以忽略其一。如果忽略类型,则类型由初始化表达式决定;忽略表达式,则初始值为该类型的零值(数字-0、布尔值-false、字符串-""、接口和引用类型-nil、数组或结构体等复杂类型-所有元素或成员的零值)。零值保障机制使得Go语言中不存在未初始化的变量,简化了代码。
可以声明一个变量列表,并选择使用对应的表达式列表对其初始化,忽略类型允许声明多个不同类型的变量:
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返回一个文件和一个错误
1.短变量声明
name := expression //name的类型由expression决定
anim := gif.GIF{LoopCount: nframes}
freq := rand.Float64() * 3.0
t := 0.0
因其短小、灵活,故在局部变量的声明和初始化中主要使用短声明。
var声明通常是为那些跟初始化表达式类型不一致的局部变量保留的,或者用于后面才对变量赋值以及变量初始值不重要的情况。
与var声明一样,多个变量也可以以短声明方式声明和初始化、调用返回等:
i, j := 0, 1
f, err := os.Open(name)
:=表示声明,=表示赋值,多变量的声明不能和多重赋值搞混
注:短变量声明不需要声明所有左边的变量,但最少要声明一个变量
in, err := os.Open(infile) //声明in和err
out, err := os.Open(outfile) //声明out,赋值err
2.指针
变量是存储值的地方,指针的值是变量的地址。使用指针,可以在不知道变量名字的情况下,间接获得或改变变量的值。
x := 1
p := &x //p是整型指针,指向x,存储x的地址
fmt.Println(*p) //*p获得p存储地址所指向的值,也就是x,打印1
*p = 2 //修改p存储地址所指向的值为2
fmt.Println(x) // 打印2
指针类型的零值是nil,指针之间可以互相比较,当两个指针指向同一个变量或者两者都是nil是才相等
var x, y int
fmt.Println(&x == &x, &x == &y, &x == nil) //true false false
函数返回局部变量的地址非常安全,如下:
//通过调用f产生的局部变量v在调用后依然存在,指针p依然引用它
var p = f()
func f()*int{
v := 1
return &v
}
fmt.Println(f() == f()) //false,每次调用f会返回不同的值,因为每次的v都是新声明的,和上一次的不同
3.new函数
另一种创建变量的方式是使用内置的new函数。
new(T)创建一个未命名的T类型变量,初始化为T类型的零值,并返回其地址(地址类型为*T)。
p := new(int) //p的类型是*int,指向未命名的int变量
fmt.Println(*p) //打印0
*p = 2
fmt.Println(*p) //打印2
使用new创建的变量和取其地址的普通局部变量没什么区别,只是不需要引入和声明一个虚拟的名字,通过new(T)就可直接在表达式中使用。
//下面两个函数的行为相同
func newInt()*int{
return new(int)
}
func newInt()*int{
var dummy int
return &dummy
}
每一次调用new返回一个具有唯一地址的不同变量,但有一个例外:两个变量的类型不携带任何信息且是零值,例如struct{}或[0]int,则它们具有相同的地址。
4.变量的生命周期
生命周期指在程序执行过程中变量存在的时间段。包级别变量的生命周期是整个程序的执行时间。局部变量有一个动态的生命周期:每次执行声明语句时创建一个新的实体,变量一直生存到它变得不可访问,这是它占用的存储空间被回收。
编译器可以选择使用堆或栈上的空间来分配。
var global *int
func f(){
var x int
x = 1
global = &x
}
func g(){
y := new(int)
*y = 1
}
上述代码中,x一定使用堆空间,因为它在f函数返回后还可以从global变量进行防卫,这种情况叫x从f中逃逸。
*y使用栈空间,g函数返回后,*y变得不可访问,可回收。
每一次变量逃逸都需要一次额外的内存分配过程。
在长生命周期对象中保持短生命周期对象不必要的指针,特别是在全局变量中,会阻止垃圾回收器回收短生命周期的对象空间。
四、赋值
赋值语句用来更新变量的值。
x = 1 //有名称的变量
*p = true //间接变量
person.name = "bob" //结构体成员
count[x] = count[x] * scale //数组或slice或map成员//赋值方式可简写为count[x] *= scale
数字变量也可以通过++和–进行递增和递减,++和–只能放在变量之后。
1.多重赋值
多重赋值允许多个变量一次性被赋值。
x, y = y, x //交换两个变量的值
//计算两个整数的最大公约数
func gcd(x, y int)int{
for y != 0{
x, y = y, x%y
}
return x
}
//计算斐波那契数列的第n个数
func fib(n int)int{
x, y := 0, 1
for i := 0; i < n; i++{
x, y = y, x+y
}
return x
}
一个有多个返回值的函数在调用时,左边的变量个数需要和函数的返回值一样多:
f, err = os.Open("foo.txt")
通常函数使用额外的返回值指示一些错误情况,如err。
2.可赋值性
赋值语句是显式形式的赋值,但程序中很多地方的赋值是隐式的,如:一个函数调用隐式地将参数的值赋给对应参数的变量;一个return语句隐式地将return操作数赋值给结果变量.
//复合类型隐式赋值
medals := []string{"gold", "silver", "bronze"}
//给每个元素隐式赋值
medals[0] = "gold"
medals[1] = "silver"
medals[2] = "bronze"
不论显式还是隐式赋值,只要左边(变量)和右边(值)类型相同就是合法的,即为可赋值性。
可赋值性根据类型不同有着不同的规则,即类型必须精准匹配,nil可以被赋给任何接口变量或引用类型。
五、类型声明
任何程序中,都有一些变量使用相同的表示方式,但含义相差非常大,如:int类型表示整数、float64类型表示精确到几位小数、string类型表示字符串。
type声明定义一个新的类型,它和某个已有类型使用同样的底层类型。
type name underlying-type
举一个简单的例子说明类型声明:
//包tempconv进行摄氏度和华氏度的转换
package tempconv
import "fmt"
type Celsius float64 //Celsius类型,与float64作用相同
type Fahrenheit float64 //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,但它们并不是相同的类型,不能进行比较或进行计算,即:从float64转换为Celsius或Fahrenheit需要显式类型转换,Celsius与Fahrenheit之间转换也需要类型转换。这不会改变值和表达方式,但改变了显式意义。
对于每个类型T,都有一个对应的类型转换操作T(x)将x的类型转换为T。若两个类型具有相同的底层类型或二者都是指向相同底层类型变量的未命名指针类型,则二者可以相互转换。
命名类型的底层类型决定了它的结构和表达方式,以及它所支持的内部操作集合,这些内部操作与直接使用底层类型的情况相同。
fmt.Printf("%g\n", BoilingC - FreezingC) //100℃
boilingF := CToF(BoilingC) //将摄氏沸点转换为华氏度,并将华氏类型的值赋给boilingF
fmt.Printf("%g\n", boilingF - CToF(FreezingC)) //180℉
fmt.Printf("%g\n", boilingF - FreezingC) //编译错误:类型不匹配
使用比较操作符的情况同上。
命名类型提供了概念上的便利,避免一遍遍重复写复杂类型,如map类型等。
六、包和文件
在Go语言中包的作用和其他语言中的库或模块作用类似,用于支持模块化、封装、编译隔离和重用。一个包的源代码保存在一个或多个以.go结尾的文件中,它所在的目录名的尾部就是包的导入路径。
每一个包给它的声明提供独立的命名空间。
我们可以通过控制变量在包外的可见性或导出情况来隐藏信息,即导出的标识符的首字母是否大写。
1.导入
在Go程序里,每一个包通过import进行导入。如果导入一个没有被引用的包,就会触发一个错误。
2.包初始化
包的初始化从包级别的变量开始,这些变量按照声明顺序初始化。如果包由多个.go文件组成,初始化按照编译器收到文件的顺序进行。
对于包级别的每一个变量,生命周期从起被初始化开始,但对于其他的一些变量,如数据表,初始化表达式不是简单地设置初始值,这种情况下,init函数的机制就比较好用了。
func init(){}
init函数不能被调用和引用,在每个文件中,当程序启动时,init函数会按照函数体内声明的顺序自动执行。
包的初始化按照在程序中导入的顺序进行,每次初始化一个包。如果包p导入了包q,可以确保q在p之前已完全初始化。初始化过程是自下而上的,main包最后初始化,确保在main函数执行前,所有的包已初始化完毕。
七、作用域
声明的作用域是指用到声明时所声明名字的源代码段。
语法块是由大括号围起来的一个语句序列,比如一个循环体或函数体。在语法块内声明的变量对块外部不可见。也可以把没有显式包含在大括号中的声明代码包含在块中,将其称为词法块。包含了全部源代码的词法块叫做全局块。
一个声明的词法块决定声明的作用域大小。包级别的声明可以被包内的任何文件引用。导入的包是文件级别的,可以在同一个文件内引用,如fmt,但不能在没有另一个import语句的前提下被同一个包中其他文件中的东西引用,即同一个包下的文件需要导入fmt包也需要import。许多声明是局部的,仅可在同一个函数中或仅仅是函数的一部分所引用。
控制流标签(break、continue、goto语句使用的标签)的作用域是整个外层函数。
在包级别,声明的顺序和它们的作用域没有关系,所以一个声明可以引用它自己或跟在它后面的其他声明。若常量或变量声明引用它自己,则编译器会报错。