2、程序结构
2.1 命名
一个名字必须以一个字母或下划线开头,后面可以跟任意数量的字母、数字或下划线。
大写字母和小写字母是不同的。
GO语言有25个关键字,关键字不能用于自定义名字。
还有大约30多个预定义名字,对应内建的常量、类型和函数,内部预定义名字可以在定义中重新使用他们,但也要避免过度而引起语义混乱。
名字是大写字母开头(必须是在函数外部定义的包级名字;包级函数名本身也是包级名字),那么它将是导出的,可以被外部的包访问。
包本身的名字一般都是用小写字母。
名字的长度没有逻辑限制,但尽量使用短小的名字。
驼峰式命名
2.2 声明
声明语句定义了程序的各种实体对象以及部分或全部的属性。
var变量、const常量、type类型、func函数实体对象
一个go语言编写的程序对应一个或多个以.go为文件后缀名的源文件中。每个源文件以包的声明语句开始(说明该源文件是属于哪个包),包声明语句之后是import语句导入依赖的其他包,然后是包一级的类型、变量、常量、函数的声明语句(包一级的各种类型的声明语句的顺序无光紧要,函数内部的名字则必须先声明之后才能使用)
go
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)
}
在包一级声明语句的名字可在整个包对应的每个源文件中访问,而不仅仅在其声明语句所在的源文件中访问,局部声明的名字就只能在函数内部很小的范围被访问。
go
package main
import "fmt"
func main(){
const freezingF,boilingF=32.0 212.0
fmt.Printf("%g F or %g C\n",freezingF,fToC(freezingF))
fmt.Printf("%g F or %g C\n",boilingF,fToC(boilingF))
}
func fToC(f float64) float64{
return (f-32)*5/9
}
2.3 变量
go
var 变量名字 类型=表达式
零值初始化机制
go
var s string
fmt.Println(s)//""
一组变量
go
var i,j,k int
var b,f,s=true,2.3,"four"
在包级别声明的变量会在main入口函数执行前完成初始化,局部变量将在声明语句被执行到的时候完成初始化。
一组变量也可以通过调用一个函数,由函数返回的多个返回值初始化
go
var f,err=os.Open(name)
2.3.1 简短变量声明
变量的类型根据表达式来自动推导,简短变量声明被广泛用于大部分的局部变量的声明和初始化。
go
变量名:=表达式
go
i:=100
j,k:=0,1
go
f,err:=os.Open(name)
简短变量声明左边的变量可能并不是全部都是刚刚声明过。如果有一些已经在相同的词法域声明过了,那么简短变量声明语句对这些已经声明过的变量就只有赋值行为了。
go
in,err:=os.Open(infile)
out,err:=os.Create(outfile)
简短变量语句中必须至少要声明一个新的变量。
简短变量声明语句只有对已经在同级词法域声明过的变量才和赋值操作语句等价,如果变量是在外部词法域声明的,那么简短变量声明语句将会在当前词法域重新声明一个新的变量。
2.3.2 指针
指针的值是另一个变量的地址。
go
& x//取变量x的内存地址
*p//读取指针指向的变量的值
go
x:=1
p:=&x
fmt.Println(*p)//1
*p=2
fmt.Println(x)//2
go
var x,y int
fmt.Println(&x==&x,&x==&y,&x==nil)//true false false
返回函数中局部变量的地址也是安全的。
go
var p=f()
func f() *int{
v:=1
return &v
}
每次调用f函数都将返回不同的结果:
go
fmt.Println(f()==f())//false
指针作为参数调用函数,可以在函数中通过该指针来更新变量的值。
go
func incr(p *int) int{
*p++//增加p指向的变量的值,并不改变p指针
return *p
}
v:=1
incr(&v)//2
fmt.Println(incr(&v))//3
*p是变量v的别名
指针是实现标准库flag包的关键技术,它使用命令行参数来设置对应变量的值,而这些对应命令行标志参数的变量可能会零散分布在整个程序中。
2.3.3 new函数
创建变量
new(T)将创建一个T类型的匿名变量,初始化为T类型的零值,然后返回变量地址,返回的指针类型为*T
go
p:=new(int)//p,*int 类型,指向匿名的int变量
fmt.Println(*p)//0
*p=2
fmt.Println(*p)//2
用new创建变量和普通变量声明语句方式创建变量没有什么区别,除了不需要声明一个临时变量的名字。
在表达式中使用new(T)
下面的两个newInt函数有着相同的行为
go
func newInt() *int{
return new(int)
}
func newInt() *int{
var dummy int
return &dummy
}
每次调用new函数都是返回一个新的变量地址,下面两个地址都是不同的:
go
p:=new(int)
q:=new(int)
fmt.Println(p==q)//false
new只是一个预定义函数,它并不是一个关键字,我们可以将new名字重新定义别的类型
go
func delta(old,new int) int {return new-old}
由于new被定义为int类型的变量名,因此delta函数内部是无法使用内置的new函数的。
2.3.4 变量的生命周期
包一级声明的变量,生命周期和整个程序的运行周期是一致的。
局部变量的声明周期则是动态的:每次从创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量的存储空间可能被回收。函数的参数变量和返回值变量都是局部变量。它们在函数每次被调用的时候创建。
函数的右小括弧也可以另起一行缩进,同时为了防止编译器在行尾自动插入分号而导致编译错误,可以在末尾的参数变量后面显式插入逗号。
go
img.SetColorIndex(
size+int(x*size+0.5),size+int(y*size+0.5),
blackIndex,
)
Go语言的==自动垃圾收集器【不需要显式地分配和释放内存】==是如何知道一个变量是何时可以被回收的?基本实现思路:从每个包级的变量和每个当前运行函数的每个当前运行函数的每一个局部变量开始,通过指针或引用的访问路径遍历,是否可以找到该变量。如果不存在这样的访问路径,那么说明该变量是不可达的,也就是说它是否存在并不影响程序后续的计算结果。
一个变量的有效周期只取决于是否可达,因此一个循环迭代内部的局部变量的生命周期可能超出其局部作用域。局部变量可能在函数返回之后依然存在。
编译器会自动选择在栈上还是在堆上分配局部变量的存储空间,这个选择并不是由var还是new声明变量的方式决定。
go
var global *int
func f(){
var x int
x=1
global=&x
}
func g(){
y:=new(int)
*y=1
}
f函数的x变量必须在堆上分配,它在函数退出后依然可以通过包一级的global变量找到,虽然它是在函数内部定义的(x局部变量从函数f中逃逸了)。
当g函数返回时,变量*y将是不可达的,可以马上被回收的,编译器可以选择在栈上分配存储空间。
2.4 赋值
go
x=1
*p=true
person.name="bob"
count[x]=count[x]*scale
count[x]*=scale
++递增和--递减语句,是语句而不是表达式x=i++是错误的
go
v:=1
v++
v--
2.4.1 元组赋值
同时更新多个变量的值
go
x,y=y,x
a[i],a[j]=a[j],a[i]
go
//计算两个整数值的最大公约数
func gcd(x,y int) int{
for y!=0{
x,y=y,x%y
}
return x
}
go
//计算斐波纳契数列的第n个数
func fib(n int)int{
x,y:=0,1
for i:=0;i<n;i++{
x,y=y,x+y
}
return x
}
元组赋值可以使一系列琐碎赋值更加紧凑,特别是在for循环的初始化部分
go
i,j,k=2,3,5
调用一个有多个返回值的函数
go
f,err=os.Open("foo.txt")
如果map查找、类型断言或通信接收出现在赋值语句的右边,它们都可能会产生两个结果,有一个额外的布尔结果表示操作是否成功:
go
v,ok=map[key]
v,ok=x.(T)
v,ok=<-ch
map查找、类型断言或通道接收出现在赋值语句的右边,并不一定是产生两个结果,也可能只产生一个结果。map查找失败时返回零值,类型断言失败时会发送运行panic异常,通道接收失败时会返回零值(阻塞不算是失败)。
我们可以用_来丢弃不需要的值。
go
_,err=io.Cpoy(dst,src)//丢弃字节数
_,ok=X.(T)//只检测类型,忽略具体值
2.4.2 可赋值性
隐式赋值行为:函数调用会隐式地将调用参数的值赋值给函数的参数变量,一个返回语句会隐式地将返回操作的值赋值给结果变量,一个复合类型的字面量也会产生赋值行为。
隐式地对slice的每个元素进行赋值操作:
go
medals:=[]string{"gold","silver","bronze"}
类型必须完全匹配,nil可以赋值给任何指针或引用类型的变量。避免不必要的显式的类型转换。
2.5 类型
类型声明语句创建一个新的类型名称:
go
type 类型名字 底层类型
类型声明语句一般出现在包一级,如果新创建的类型名字的首字符大写,则在外部包也可以使用
我们将不同温度单位分别定义为不同的类型:
go
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)}
对于每一个类型T,都有一个对应的类型转换操作T(x),将x转为T类型。
命名类型还可以为该类型的值定义新的行为。
声明Celsius类型的一个名为String方法。
go
func (c Celsius) String() string {return fmt.Sprintf("%g C",c)}
2.6 包和文件
包:为了支持模块化、封装、单独编译和代码重用。
2.6.1 导入包
每个包都有一个全局唯一的导入路径。
包名在包的声明处指定。
import 包名
如果导入了一个包,但是又没有使用该包将当作一个编译错误处理。
goimports工具,根据需要自动添加或删除导入的包。
2.6.2 包的初始化
包的初始化首先是解决包级变量的依赖顺序,然后按照包级变量声明出现的顺序依次初始化。
go
var a=b+c //3
var b=f() //2
var c=1 //1
func f() int {return c+1}
如果包中含有多个.go源文件,它们将按照发给编译器的顺序进行初始化
初始化表达式初始化,特殊的init初始化函数来简化初始化工作
每个包在解决依赖的前提下,以导入声明的顺序初始化,每个包只会被初始化一次。
匿名函数
2.7 作用域
声明语句的作用域是指源代码中可以有效使用这个名字的范围。
不要将作用域和生命周期混淆。生命周期是程序运行时变量存在的有效时间段,在此时间区域内他可以被程序的其他部分引用,是一个运行时的概念。
句法块
词法块
语法块
要特别注意短变量声明语句的作用域范围。