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