概述
简介
在函数内部,可以省略var关键字,使用更简单的定义模式。
1 | fun main() { |
流程控制可以省略条件判断:
1 | switch { |
for x < 5相当于while(x < 5),for相当于while(true)。
在迭代遍历时,for i, n := range x可以返回索引。
函数是第一类型,可以作为参数或返回值。
1 | func test(x int) func() { // 返回函数类型 |
结构体可以匿名嵌入其它类型:
1 | type user struct { |
类型
变量
运行时内存分配操作确保变量自动初始化为二进制零值,避免出现不可预测的行为。
建议以组方式整理多行变量定义。
1 | var ( |
易错点:
1 | var x = 100 |
在进行多变量赋值操作时,首先计算出所有的右值,然后再依次完成赋值操作。
1 | func main() { |
1 | go build |
常量
可在函数代码块中定义常量,不曾使用的常量不会引发编译错误。
如果显示指定类型,必须确保常量左右值类型一致,需要时可做显示转换。右值不能超出常量类型取值范围,否则会引发溢出错误。
1 | const { |
常量值也可以是某些编译器能计算出结果的表达式,如unsafe.Sizeof、len、cap等。
在常量组中,如不指定类型和初始化值,则与上一行非空常量右值相同。
1 | const ( |
如中断iota自增,则必须显示恢复。
1 | const ( |
自增默认数据类型为int,可显示指定类型。在实际编码中,建立用自定义类型实现用途明确的枚举类型。
1 | const ( |
常量不会分配存储空间,通常在编译器预处理阶段直接展开,作为指令数据使用。无需像变量哪有通过内存寻址来取值,因此无法获取地址。
1 | const y = 0x200 |
基本类型
标准库strconv可在不同进制(字符串)之间转换
1 | import "strconv" |
默认浮点数类型是float64
注意别名
1 | byte alias for uint8 |
64位平台上int和int64结构完全一致,也分属不同类型,需显式转换
1 | var x int = 100 |
引用类型
内置函数new按指定类型长度分配零值内存,返回指针,并不关心类型内部构造和初始化方式。而引用类型则必须使用make函数创建,编译器会将make转换为目标类型专用的创建函数或指令,以确保完成全部内存分配和相关属性初始化。
1 | func mkmap() map[string]int { |
new函数也为引用类型分配内存,但这是不完整创建,仅分配类型本身所需内存(实际就是个指针包装),并没有分配键值存储内存,也没有初始化散列桶等内部属性,因此无法正常工作。
1 | p := new(map[string]int) // 返回指针 |
类型转换
如果转换的目标是指针、单向通道或没有返回值的函数类型,那么必须使用括号,以避免造成语法分解错误。
1 | x := 100 |
自定义类型
和var、const类似,多个type定义可合并成组,可在函数或代码块内定义局部类型。
1 | type ( // 组 |
即便指定了基础类型,也只表明它们有相同的底层数据结构,两者间不存在任何关系,属于完全不同的两种类型。除了操作符外,自定义类型不会继承基础类型的其他信息,包括方法。不能视作别名,不能隐式转换,不能直接用于比较表达式。
1 | type data int |
与有明确标识符的bool、int、string等类型相比,数组、切片、字典、通道等类型与具体元素类型或长度等属性有关,故称作未命名类型,可用type为其提供具体名称,将其改变为命名类型。
具有相同声明的未命名类型被视作同一类型。
- 具有相同基类型的指针。
- 具有相同元素类型和长度的数组array。
- 具有相同元素类型的切片slice。
- 具有相同键值类型的字典map。
- 具有相同数据类型及操作方向的通道channel。
- 具有相同字段序列(字段名、字段类型、标签,以及字段顺序)的结构体。
- 具有相同签名(参数和返回值列表,不包含参数名)的函数。
- 具有相同方法集(方法名、方法签名,不包括顺序)的接口。
容易被忽视的是struct tag,它也是属于类型的组成部分,而不仅仅是原数据描述。
1 | var a struct { |
同样,函数的参数顺序也属于签名组成部分。
1 | var a func(int, string) |
未命名类型转换规则:
- 所属类型相同
- 基础类型相同,且其中一个是未命名类型
- 数据类型相同,将双向通道赋值给单向通道,且其中一个是未命名类型
- 将默认值nil赋值给切片、字典、通道、指针、函数或接口。
- 对象实现了目标接口。
1 | type data [2]int |
表达式
运算符
除位移操作外,操作数类型必须相同。如果其中一个是无显式类型声明的常量,那么该常量操作数会自动转型。
1 | const v = 20 // 无显式类型声明的常量 |
位移右操作数必须是无符号整数,或可以转换的无显式类型常量。如果是非常量位移表达式,那么会优先将无显式类型的常量左操作数转型。
1 | a := 1.0 << 3 // 常量表达式(包括常量展开) |
按位清除,a &^ b 比如 0110 &^ 1011 = 0100
自增、自减不再是运算符,只能作为独立语句,不能用于表达式。
1 | a := 1 |
并非所有对象都能取地址操作,但变量总是能正确返回。
1 | m := map[string]int{"a": 1} |
指针类型支持相等运算符,但不能做加减法运算和类型转换。如果两个指针指向同一个地址,或都是nil,那么它们相等。
1 | x := 10 |
可通过unsafe.Pointer将指针转换为unintptr后进行加减运算,但可能会造成非法访问。Pointer类似C语言中的void *万能指针,可用来转换指针类型。它能安全持有对象或对象成员,但uintptr不行。后者仅是一种特殊整形,并不引用目标对象,无法阻止垃圾回收器回收对象内存。
零长度对象的地址是否相等和具体的实现版本有关,不过肯定不等于nil。
1 | var a, b struct{} |
即便长度为0,可该对象依然是“合法存在”的,拥有合法内存地址,这与nil语义完全不同。
在runtime/malloc.go里有个zerobase全局变量,所有通过mallocgc分配的零长度对象都使用该地址。不过上例中,对象a、b在栈上分配,并未调用mallocgc函数。
初始化
对复合类型(数组、切片、字典、结构体)变量初始化时,有一些语法限制。
- 初始化表达式必须包含类型标签。
1 | var a data = data{1, "abc"} |
流控制
比较特别的是对初始化语句的支持,可定义块局部变量或执行初始化函数。
1 | if xinit(); x == 0 { // 优先执行xinit函数 |
多个case匹配条件,命中其中一个即可。
1 | switch x { |
switch同样支持初始化语句,按从上到下、从左到右顺序匹配case执行。只有全部匹配失败时,才会执行default块。
1 | switch x := 5; x { |
相邻的空case不构成多条件匹配。
1 | switch x { |
不能出现重复的case常量值。
1 | func main() { |
无须显式执行break语句,case执行完毕后自动中断。如须贯通后续case(源码顺序),须执行fallthrough,但不再匹配后续条件表达式。
1 | switch x := 5; x { |
fallthrough必须放在case块结尾,可使用break语句阻止。
1 | switch x := 5; x { |
某些时候,switch还被用来替换if语句。被省略的switch条件表达式默认值为true,继而与case比较表达式结构匹配。
1 | switch x := 5; { // 相当于"switch x := 5; true { ... }" |
仅有for一种循环语句,但常用方式都支持。
1 | for i := 0; i < 3; i++ { |
初始化语句仅被执行一次。条件表达式中如有函数调用,须确认是否会重复执行。可能会被编译器优化掉,也可能是动态结果须每次执行确认。
1 | for i, c := 0, count(); i < c; i++ { // 初始化语句的count函数仅执行一次 |
函数
函数只能判断其是否为nil,不支持其他比较操作。
从函数返回局部变量指针是安全的,编译器会通过逃逸分析来决定是否在堆上分配内存,如果运行函数内联,局部变量可能直接分配到栈上。
1 | func test() *int { |
表面上看,指针类型的行参性能更好一些,但实际上得具体分析。被复制的指针会延长目标对象生命周期,还可能会导致它被分配到堆上,那么其性能消耗就得加上堆内存分配和垃圾回收的成本。
1 | func test(p *int) { // 延长p生命周期 |
变参本质上就是一个切片。只能接收一到多个同类型参数,且必须放在列表尾部。
1 | func test(s string, a ...int) { |
将切片作为变参时,需要进行展开操作。如果是数组,先将其转换为切片。
1 | func test(a ...int) { |
命名返回值和参数一样,可当作函数局部变量使用,最后由return隐式返回。
1 | func div(x, y int) (z int, err error) { |
如果返回值类型能明确表示其含义,就尽量不要对其命名。
1 | func NewUser() (*User, error) |
v1.5.2