go语言学习笔记-语言详解

概述

简介

在函数内部,可以省略var关键字,使用更简单的定义模式。

1
2
3
4
fun main() {
x := 100
fmt.Println(x)
}

流程控制可以省略条件判断:

1
2
3
4
5
6
7
8
switch {
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
5
func test(x int) func() { // 返回函数类型
return func() { // 匿名函数
println(x) // 闭包
}
}

结构体可以匿名嵌入其它类型:

1
2
3
4
5
6
7
8
9
type user struct {
name string
age byte
}

type manager struct {
user // 匿名嵌入其它类型,类似于继承的功能
title string
}

类型

变量

运行时内存分配操作确保变量自动初始化为二进制零值,避免出现不可预测的行为。

建议以组方式整理多行变量定义。

1
2
3
4
var (
x, y int
a, s = 100, "abc"
)

易错点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var 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
7
func main() {
x, y := 1, 2
x, y = y+3, x+2 // 先计算右值y+3, x+2,然后再对x, y变量赋值

println(x, y)
// 5 3
}

1
2
go build
go tool objdump -s "main\.main" test

常量

可在函数代码块中定义常量,不曾使用的常量不会引发编译错误。

如果显示指定类型,必须确保常量左右值类型一致,需要时可做显示转换。右值不能超出常量类型取值范围,否则会引发溢出错误。

1
2
3
4
5
const {
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
6
const (
x uint16 = 120
y // uint16 120
s = "abc"
z // string "abc"
)

如中断iota自增,则必须显示恢复。

1
2
3
4
5
6
7
8
const (
a = iota // 0
b // 1
c = 100
d // 100
e = iota // 4,需要包括c,d
f // 5
)

自增默认数据类型为int,可显示指定类型。在实际编码中,建立用自定义类型实现用途明确的枚举类型。

1
2
3
4
5
const (
a = iota // int
b float32 = iota // float32
c = iota // int(如不显示指定iota,则与b数据类型相同)
)

常量不会分配存储空间,通常在编译器预处理阶段直接展开,作为指令数据使用。无需像变量哪有通过内存寻址来取值,因此无法获取地址。

1
2
3
4
const 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
17
import "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
6
byte alias for uint8
rune alias for int32

var a byte = 0x11
var b uint8 = a // 别名类型无需转换,直接赋值
var c uint8 = a + b

64位平台上int和int64结构完全一致,也分属不同类型,需显式转换

1
2
var 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
10
func mkmap() map[string]int {
m := make(map[string]int)
m["a"] = 1
return m
}

func main() {
m := mkmap()
println(m["a"])
}

new函数也为引用类型分配内存,但这是不完整创建,仅分配类型本身所需内存(实际就是个指针包装),并没有分配键值存储内存,也没有初始化散列桶等内部属性,因此无法正常工作。

1
2
3
p := new(map[string]int) // 返回指针
m := *p
m["a"] = 1 // panic: assignment to entry in nil map(运行期错误)

类型转换

如果转换的目标是指针、单向通道或没有返回值的函数类型,那么必须使用括号,以避免造成语法分解错误。

1
2
3
4
5
x := 100
// p := *int(&x)
p := (*int)(&x) // 正确写法
(<-chan int)(c)
(func())(x)

自定义类型

和var、const类似,多个type定义可合并成组,可在函数或代码块内定义局部类型。

1
2
3
4
5
6
7
type ( // 组
user struct { // 结构体
name string
age uint8
}
event func(string) bool // 函数类型
)

即便指定了基础类型,也只表明它们有相同的底层数据结构,两者间不存在任何关系,属于完全不同的两种类型。除了操作符外,自定义类型不会继承基础类型的其他信息,包括方法。不能视作别名,不能隐式转换,不能直接用于比较表达式。

1
2
3
4
5
6
type 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
13
var 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
5
var 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
2
3
4
5
6
type data [2]int
var d data = [2]int{1, 2} // 基础类型相同,右值为未命名类型

a := make(chan int, 2)
var b chan<- int = a // 双向通道转换为单向通道,其中b为未命名类型
b <- 2

表达式

运算符

除位移操作外,操作数类型必须相同。如果其中一个是无显式类型声明的常量,那么该常量操作数会自动转型。

1
2
3
4
5
6
const 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
9
a := 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
7
a := 1
++a // unexpected ++ (不能前缀)
if (a++) > 1 { // 不能作为表达式使用
}

p := &a
*p++ /// 相当于(*p)++

并非所有对象都能取地址操作,但变量总是能正确返回。

1
2
3
m := 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
8
x := 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
2
var a, b struct{}
println(&a == &b, &a == nil) // true false

即便长度为0,可该对象依然是“合法存在”的,拥有合法内存地址,这与nil语义完全不同。
在runtime/malloc.go里有个zerobase全局变量,所有通过mallocgc分配的零长度对象都使用该地址。不过上例中,对象a、b在栈上分配,并未调用mallocgc函数。

初始化

对复合类型(数组、切片、字典、结构体)变量初始化时,有一些语法限制。

  • 初始化表达式必须包含类型标签。
1
2
var a data = data{1, "abc"}
var d data = {1, "abc"} // error: 缺类型标签

流控制

比较特别的是对初始化语句的支持,可定义块局部变量或执行初始化函数。

1
2
3
if xinit(); x == 0 { // 优先执行xinit函数
println("a")
}

多个case匹配条件,命中其中一个即可。

1
2
3
4
switch x {
case a, b: // 支持非常量值
println("a | b")
}

switch同样支持初始化语句,按从上到下、从左到右顺序匹配case执行。只有全部匹配失败时,才会执行default块。

1
2
3
4
5
6
7
8
switch x := 5; x {
case 5:
x += 50
println(x)
default: // 编译器确保不会先执行default块
x += 100
println(x)
}

相邻的空case不构成多条件匹配。

1
2
3
4
5
switch x {
case a: // 单条件,内容为空。隐式"case a: break;"
case b:
println("b")
}

不能出现重复的case常量值。

1
2
3
4
5
6
7
8
func 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
14
switch 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
15
switch 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
8
switch 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
10
for 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
9
for 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
9
func 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
11
func test(p *int) { // 延长p生命周期
go func() {
println(p)
}()
}

func main() {
x := 100
p := &x
test(p)
}

变参本质上就是一个切片。只能接收一到多个同类型参数,且必须放在列表尾部。

1
2
3
4
5
6
7
8
func 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
8
func test(a ...int) {
fmt.Println(a)
}

func main() {
a := [3]int{10, 20, 30}
test(a[:]...)
}

命名返回值和参数一样,可当作函数局部变量使用,最后由return隐式返回。

1
2
3
4
5
6
7
8
func 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)

nephen wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!
坚持原创技术分享,您的支持将鼓励我继续创作!