基本语法
变量
定义
关键字 var 用于定义变量,和 C 不同,类型被放在变量名后。
另外,运行时内存分配操作会确保变量自动初始化为二进制零值(zero value),避免出现不可预测行为。
如显式提供初始化值,可省略变量类型,由编译器推断
| var x int // 自动初始化为 0
var y = false // 自动推断为 bool 类型
|
可一次定义多个变量,包括用不同初始值定义不同各类型
| var x, y int // 相同类型的多个变量
var a, s = 100, "abc" // 不同类型初始化值
|
依照惯例,建议以组方式整理多行变量定义
| var (
x, y int
a, s = 100, "abc"
)
|
简短模式
除 var 关键字外,还可使用更加简短的变量定义和初始化语法
| func main() {
x := 100
a, s := 1, "abc"
}
|
只是要注意,简短模式(short variable declaration)有些限制:
- 定义变量,同时显式初始化
- 不能提供数据类型
- 只能用在函数内部。
对于粗心的新手,这可能会造成意外错误。
比如原来打算修改全部变量,结果变成重新定义同名局部变量
| package main
var x = 100
func main() {
println(&x, x) // 全局变量
x := "abc" // 重新定义和初始化同名局部变量
println(&x, x)
}
|
输出结果:
| // 对比内存地址,可以看出是两个不同的变量
0x10c8190 100
0xc000038768 abc
|
简短定义在函数多返回值,以及if/for/switch
等语句中定义局部变量非常方便
简短模式并不是重新定义变量,有可能是部分退化的赋值操作
| package main
func main() {
x := 100
println(&x)
x, y := 200, "abc" // 注意: x 退化为赋值操作,仅有 y 是变量定义
println(&x, x)
println(y)
}
|
输出:
| 0xc000038770
0xc000038770 200
abc
|
退化赋值的前提条件是: 最少有一个新变量被定义,且必须是同一作用域
| package main
func main() {
x := 100
println(&x)
x := 200 // 错误:no new variables on left side of :=
println(&x, x)
}
|
| package main
func main() {
x := 100
println(&x)
{
x, y := 200, 300 // 不同作用域,全部是新变量定义
println(&x, x, y)
}
}
|
输出:
| 0xc000038770
0xc000038768 200 300
|
在处理函数错误返回值时,退化赋值允许我们重复使用 err 变量,这是相当有益的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | package main
import (
"log"
"os"
)
func main() {
f, err := os.Open("dev/random")
...
buf := make([]byte, 1024)
n, err := f.Read(buf) // err 退化赋值,n 新定义
...
}
|
多变量赋值
在进行多变量赋值操作时,首先计算出所有右值,然后再依次完成赋值操作
| package main
func main() {
x, y := 1, 2
x, y = y+3, x+2 // 先计算出右值 y + 3、x + 2,然后再对 x、y 变量赋值
println(x, y)
}
// 输出结果: 5 3
|
编译器将未使用局部变量当作错误。不要觉得麻烦,这有助于培养良好的编码习惯
| package main
var x int // 全局变量没问题
func main() {
y := 10
}
|
| ./test.go:6:2: y declared but not used
|
Go 语言的每一个变量都拥有自己的类型,必须经过声明才能开始用
| var a int
var b string
var c []float32
var d func() bool
var e struct {
x int
}
|
上面代码的共性是,以 var 关键字开头,要声明的变量名放在中间,而将其类型放在后面
批量定义变量的方法
| var (
a int
b string
c []float32
d func() bool
e struct {
x int
}
)
|
初始化变量
Go 语言在声明变量时,自动对变量对应的内存区域进行初始化操作。
每个变量会初始化其类型的默认值
- 整形和浮点型变量的默认值为 0
- 字符串变量的默认值为空字符串
- 切片、函数、指针变量的默认为 nil
当然,依然可以在变量声明时赋予变量一个初始值
回顾:
在 C 语言中,变量在声明时,并不会对变量对应的内存区域进行清理操作。
此时,变量值可能是完全不可预期的结果。
开发者需要习惯在使用 C 语言进行声明时要初始化操作,稍有不慎,就会造成不可预知的后果
在网络上只有程序员才能看懂的“烫烫烫”和“屯屯屯”的梗,就来源于 C/C++ 中变量默认不初始化
微软的 VC 编译器会将未初始化的栈空间以 16 进制的 0xCC 填充,而未初始化的堆空间使用 0xCD 填充,而 0xCCCC 和 0xCDCD 在中文的 GB2312 编码中刚好对应 “烫” 和“屯”字
因此,如果一个字符串没有结束符 "\0",直接输出的内存数据转换为字符串刚好对应“烫烫烫”和“屯屯屯”
标准格式
例如:游戏中,玩家的血量初始值为 100
编译器推导类型的格式
在标准格式的基础上,将 int 省略后,编译器会尝试根据等号右边的表达式推导 hp 变量的类型
等号右边的部分在编译原理里被称作“右值”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53 | package main
import "fmt"
var aa = 3
var kk = "kkk"
// bb := true 这种全局写法是不行的, := 只能在函数内使用
var bb = true
var (
x = 3
y = "kkk"
z = true
)
func variableZeroValue() {
var a int
var s string
fmt.Printf("%d %q\n", a, s)
}
func varibaleInitialValue() {
var a, b int = 3, 4
var s string = "abc"
fmt.Println(a, b, s)
}
func variableTypeDecuction() {
var a, b, c, s = 3, 4, true, "def"
fmt.Println(a, b, c, s)
}
func variableShorter() {
a, b, c, s := 3, 4, true, "def"
b = 5
fmt.Println(a, b, c, s)
}
func main() {
fmt.Println("Hello World")
variableZeroValue()
varibaleInitialValue()
variableTypeDecuction()
variableShorter()
fmt.Println(x, y, z)
}
// Hello World
// 0 ""
// 3 4 abc
// 3 4 true def
// 3 5 true def
// 3 kkk true
|
命名
对变量、常量、函数、自定义类型进行命名,通常优先选用有实际含义,易于阅读和理解的字母或单词组合
命名建议
- 以字母或下划线开始,由多个字母、数字和下划线组合而成
- 区分大小写
- 使用驼峰(camel case)拼写格式
- 局部变量优先使用短名
- 不要使用保留关键字
- 不建议使用与预定义常量、类型、内置函数相同的名字
- 专用名词通常会全部大写
尽管 Go 支持用汉字等 Unicode 字符命名,但从编程习惯上来说,这并不是好选择
符号名字首字母大小写决定了其作用域。
首字母大写的为导出成员,可被包外引用,而小写则仅能在包内使用
名字的长度没有逻辑限制,但是Go语言的风格是尽量使用短小的名字,对于局部变量尤其是这样;
你会经常看到i之类的短名字,而不是冗长的 theLoopIndex 命名。
通常来说,如果一个名字的作用域比较大,生命周期也比较长,那么用长的名字将会更有意义。
在习惯上,Go语言程序员推荐使用驼峰式
命名,当名字有几个单词组成的时优先使用大小写分隔,而不是优先用下划线分隔。
因此,在标准库有 QuoteRuneToASCII
和 parseRequestLine
这样的函数命名,但是一般不会用 quote_rune_to_ASCII
和 parse_request_line
这样的命名。
而像 ASCII
和 HTML
这样的缩略词则避免使用大小写混合的写法,它们可能被称为 htmlEscape
、HTMLEscape
或 escapeHTML
,但不会是 escapeHtml
。
空标识符
和 Python 类似,Go 也有个名为_
的特殊成员。
通常作为忽略占位符使用,可作表达式左值,无法读取内容。
| import "strconv"
func main() {
x, _ := strconv.Atoi("12") // 忽略 Atoi 的 err 返回值
println(x)
}
|
空标识符可用来临时规避编译器未使用变量和导入包的错误检查。
但请注意,它是预置成员,不能重新定义。
常量与枚举
常量
常量表示运行时恒定不可改变的值,通常是一些字面量。
使用常量就可用一个易于阅读理解的标识符号来代替“魔法数字”,也使得在调整常量值时,无须修改所有引用代码。
常量值必须是编译器可确定的字符、字符串、数字或布尔值。
可指定常量类型,或由编译器通过初始化值推断,不支持 C/C++ 数字类型后缀
常量表达式的值在编译期计算,而不是在运行期。每种常量的潜在类型都是基础类型:boolean、string或数字。
一个常量的声明语句定义了常量的名字,和变量的声明语法类似,常量的值不可修改,这样可以防止在运行期被意外或恶意的修改。
例如,常量比变量更适合用于表达像 π
之类的数学常数,因为它们的值不会发生变化:
| const pi = 3.14159 // approximately; math.Pi is a better approximation
|
和变量声明一样,可以批量声明多个常量;这比较适合声明一组相关的常量:
| const (
e = 2.71828182845904523536028747135266249775724709369995957496696763
pi = 3.14159265358979323846264338327950288419716939937510582097494459
)
|
所有常量的运算都可以在编译期完成,这样可以减少运行时的工作,也方便其他编译优化。
当操作数是常量时,一些运行时的错误也可以在编译时被发现,例如整数除零、字符串索引越界、任何导致无效浮点数的操作等。
常量间的所有算术运算、逻辑运算和比较运算的结果也是常量,对常量的类型转换操作或以下函数调用都是返回常量结果:len
、cap
、real
、imag
、complex
和 unsafe.Sizeof
。
因为它们的值是在编译期就确定的,因此常量可以是构成类型的一部分,例如用于指定数组类型的长度:
| const IPv4Len = 4
// parseIPv4 parses an IPv4 address (d.d.d.d).
func parseIPv4(s string) IP {
var p [IPv4Len]byte
// ...
}
|
一个常量的声明也可以包含一个类型和一个值,但是如果没有显式指明类型,那么将从右边的表达式推断类型。
在下面的代码中,time.Duration是一个命名类型,底层类型是int64,time.Minute是对应类型的常量。下面声明的两个常量都是time.Duration类型,可以通过%T
参数打印类型信息:
| const noDelay time.Duration = 0
const timeout = 5 * time.Minute
fmt.Printf("%T %[1]v\n", noDelay) // "time.Duration 0"
fmt.Printf("%T %[1]v\n", timeout) // "time.Duration 5m0s"
fmt.Printf("%T %[1]v\n", time.Minute) // "time.Duration 1m0s"
|
如果是批量声明的常量,除了第一个外其它的常量右边的初始化表达式都可以省略,如果省略初始化表达式则表示使用前面常量的初始化表达式写法,对应的常量类型也一样的。例如:
| const (
a = 1
b
c = 2
d
)
fmt.Println(a, b, c, d) // "1 1 2 2"
|
如果只是简单地复制右边的常量表达式,其实并没有太实用的价值。但是它可以带来其它的特性,那就是iota常量生成器语法。
| const x, y int = 123, 0x22
const s = "hello, world!"
const c = '我' // rune(unicode code point)
const (
i, f = 1, 0.123 // int, float64(默认)
b = false
)
|
可在函数代码块中定义常量,不曾使用的常量不会引发编译错误.
1
2
3
4
5
6
7
8
9
10
11
12 | package main
func main() {
const x = 123
println(123)
const y = 1.23 // 未使用,不会引发编译错误
{
const x = "abc" // 在不同作用域定义同名常量
println(x)
}
}
|
如果显式指定类型,必须确保常量左右值类型一致,需要时可做显式转换。
右值不能超出常量类型取值范围,否则会引发溢出错误
| const (
x, y int = 99, -999
b byte = byte(x) // x 被指定为 int 类型,须显式转换为 byte 类型
)
n = uint8(y) // 错误: constant -99 overflows uint8
|
常量值也可以是某些编译器能计算出结果的表达式,如 unsafe、Sizeof、len、cap 等
| import "unsafe"
const (
ptrSize = unsafe.Sizeof(uintptr(0))
strSize = len("hello,world!")
)
|
在常量组中如不指定类型和初始化值,则与上一行非空常量右值(表达式文本)相同
1
2
3
4
5
6
7
8
9
10
11
12
13
14 | package main
import "fmt"
func main() {
const (
x uint16 = 120
y // 与上一行 x 类型、右值相同
s = "abc"
z // 与 s 类型、右值相同
)
fmt.Printf("%T, %v\n", y, y) // 输出类型和值
fmt.Printf("%T, %v\n", z, z)
}
|
输出:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 | package main
import (
"fmt"
"math"
)
func consts() {
// const filename = "abc.txt"
// const a, b = 3, 4
const (
filename = "abc.txt"
a, b = 3, 4
)
var c = int(math.Sqrt(a*a + b*b))
fmt.Println(filename, c)
}
func main() {
consts()
}
// abc.txt 5
|
枚举
Go 并没有明确意义上的 enum 定义,不过可借助 iota 标识符实现一组自增常量值来实现枚举类型
1
2
3
4
5
6
7
8
9
10
11
12 | const (
x = iota // 0
y // 1
z // 2
)
const (
_ = iota //0
KB = 1 << (10 * iota) // 1 << (10 * 1)
MB // 1 << (10 * 2)
GB // 1 << (10 * 3)
)
|
自增作用范围为常量组。可在多常量自定义中使用多个 iota,它们各自单独计数,只需确保组中每行常量的列数量相同即可
| const (
_, _ = iota, iota * 10 // 0, 0 * 10
a, b // 1, 1 * 10
c, d // 2, 2 * 10
)
|
如中断 iota 自增,则必须显式恢复。且后续自增值按行序递增
| const (
a = iota // 0
b // 1
c = 100 // 100
d // 100(与上一行常量右值表达式相同)
e = iota // 4(恢复 iota 自增,计数包括 c、d)
f // 5
)
|
自增默认数据类型为 int,可显式指定类型
| const (
a = iota // int
b float32 = iota // float32
c = iota // int(如不显式指定 iota,则与 b 数据类型相同)
)
|
在实际编码中,建议用自定义类型实现用途明确的枚举类型。
但这并不能将取值范围限定在预定义的枚举值内
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 | type color byte // 自定义类型
const (
black color = iota // 指定常量类型
red
blue
)
func test(c color) {
println(c)
}
func main() {
test(red)
test(100) // 100 并未超出 color/byte 类型取值范围
x := 2
test(x) // 错误: cannot use x(type int) as type color in argument to test
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37 | package main
import (
"fmt"
)
func enums() {
// const (
// cpp = 0
// java = 1
// python = 2
// golang = 3
// )
const (
cpp = iota
java
python
golang
javascript
)
const (
b = 1 << (10 * iota)
kb
mb
gb
tb
pb
)
fmt.Println(cpp, java, python, golang)
fmt.Println(b, kb, mb, gb, tb, pb)
}
func main() {
enums()
}
// 0 1 2 3
// 1 1024 1048576 1073741824 1099511627776 1125899906842624
|
不同于变量在运行期分配存储内存(非优化状态),常量通常会被编译器在预处理阶段直接展开,作为指令数据使用
枚举类型判断是否相等:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 | package main
type color byte // 自定义类型
const (
black color = iota // 指定常量类型
red
blue
)
func test(c color) {
println(c)
println(c == blue)
println(c == red)
}
func main() {
test(red)
}
|
输出结果:
iota
我们知道 iota 常用于 const 表达式中,其值是从 0 开始的,const 声明块中每增加一行,iota 值自增 1
使用 iota 可以简化常量定义,但其规则必须要牢牢掌握,否则在阅读源码时可能会造成误解或障碍
(1) 下面每个常量的值是多少?
| type Priority int
const (
LOG_EMERG Priority = iota
LOG_ALERT
LOG_CRIT
LOG_ERR
LOG_WARNING
LOG_NOTICE
LOG_INFO
LOG_DEBUG
)
|
解答:
以上常量声明来源于 Go 标准库 syslog,每个常量代表一个日志级别,常量类型为 Priority,实际为 int 类型
iota 在常量声明语句汇总的初始值为 0,即 LOG_EMERG 的值为 0,下面每个常量递增 1
(2) 下面每个常量的值是多少?
| const (
mutexLocked = 1 << iota // mutex is locked
mutexWoken
mutexStarving
mutexWaiterShift = iota
starvationThresholdNs = 1e6
)
|
解答:
以上常量声明来源于 Go 标准库 sync,每个常量均代表互斥锁的特定状态
- mutexLocked 的值为 1
- mutexWoken 的值为 2
- mutexStarving 的值为 4
- mutexWaiterShift 的值为 3
- srarvationThresholdNs 的值为 1000000
(3) 下面每个常量的值是多少?
| const (
bit0, mask0 = 1 << iota, 1 << iota - 1
bit1, mask1
_, _
bit3, mask3
)
|
解答:
- bit0 的值为 1,mask0 的值为 0
- bit1 的值为 2,mask1 的值为 1
- bit3 的值为 8,mask3 的值为 7
在常量声明语句中,iota 往往用于声明连续的整型常量。iota 的取值与其出现的位置强相关
关于 iota 的取值规则,有些书上或博客中可能给出类似下面的描述:
- iota 在 const 关键字出现时被重置为 0
- const 声明块中每新增一行,iota 值自增 1
这种描述本身没有错误,在简单的语句中套用这种规则可以快速地计算 iota 的取值,但在面对复杂的语句时这种规则往往充满歧义。
笔者也曾经这么理解 iota,但在阅读各种复杂源码时经常心生困惑而不知如何计算,究其原因是这种描述没有准确地描述 iota 的本意
实际上从编译器的角度看 iota,其取值规则只有一条
- iota 代表了 const 声明块的行索引(下标从 0 开始)
这样理解更贴近编译器的实现逻辑,也更准确。
除此之外,const 声明还有一个特点,即如果为常量指定了一个表达式,但后续的常量没有表达式,则继承上面的表达式
接下来,我们根据这个规则来分析一下复杂的常量声明:
| const (
bit0, mask0 = 1 << iota, 1 << iota - 1 // const 声明第 0 行,即 iota == 0
bit1, mask1 // const 声明第 1 行,即 iota == 1,表达式继承上面的语句
_, _ // const 声明第 2 行,即 iota == 2
bit3, mask3 // const 声明第 3 行,即 iota == 3
)
|
- 第 0 行的表达式展开即
bit0, mask0 = 1 << 0, 1 << 0 - 1
,所以 bit0 == 1, mask0 == 0
- 第 1 行没有指定表达式继承第一行,即
bit1, mask1 = 1 << 1, 1 <<1 - 1
,所以 bit1 == 2, mask1 == 1
- 第 2 行没有定义常量
- 第 3 行没有指定表达式继承第一行,即
bit3, mask3 = 1 << 3, 1 << 3 - 1
,所以 bit3 = 8, mask3 == 7
itoa 实现原理
iota 标识符仅能用于常量声明语句中,它的取值与常量声明块中的代码的行数强相关,可以说它标识的正是常量声明语句中的行数(由 0 开始)。
那么它为什么会表现出这样的行为呢?
答案在于编译器处理常量声明语句的方式
在编译器代码中,每个常量声明语句使用 ValueSpec 结构表示:
| // A ValueSpec node represents a constant or variable declaration
// (ConstSpec or VarSpec production).
type ValueSpec struct {
Doc *CommentGroup // associated documentation; or nil
Names []*Ident // value names (len(Names) > 0)
Type Expr // value type; or nil
Values []Expr // initial values; or nil
Comment *CommentGroup // line comments; or nil
}
|
ValueSpec 结构不仅可以用来表示常量声明,还可以表示变量声明,不过它仅表示一种声明语句,比如:
| const (
// 常量的注释(文档)
a, b = iota, iota // 常量的行注释
)
|
上面的常量声明块中仅包括一行声明语句,该语句对应一个 ValueSpec 结构
- Doc 表示块注释,往往会出现在文档的注释中
- Name 表示常量的名字,使用切片表示单行语句中声明的多个常量
- Type 为常量类型
- Value 为常量值,与 Name 对应,表示常量的初始值
- Comment 表示行注释
编译器在构造常量时,实际上会遍历 ValueSpec 结构中的 Names 切片来逐个生成常量。
相关代码比较复杂,这里我们给出构造常量的伪算法,从中可以看出 iota 的作用
通常 const 语句块中会包含多行常量声明,那么就会对应多个 ValueSpce 结构,我们使用 ValueSpecs 表示多个 ValueSpec 结构,编译器构造常量的过程如下:
| for iota, spec := range ValueSpecs {
for i, name := range spce.Names {
obj := NewConst(name, iota...)
...
}
}
|
由上面的代码可以看出 iota 的本质,它仅代表常量声明的索引,所以它会表现出以下特征:
- 单个 const 声明块中从 0 开始取值
- 单个 const 声明块中,每增加一行声明,iota 的取值增 1,即便声明中没有使用 iota 也是如此
- 单行声明语句中,即便出现多个 iota,iota 的取值也保持不变
内建变量类型
类型 |
长度 |
默认值 |
说明 |
bool |
1 |
false |
|
byte |
1 |
0 |
uint8 |
int, uint |
4,8 |
0 |
默认整数类型,依据目标平台,32 或 64 位 |
int8, uint8 |
1 |
0 |
-128~127, 0~255 |
int16, uint16 |
2 |
0 |
-32768~32767, 0~65535 |
int32,uint32 |
4 |
0 |
-21亿~21亿 0~42亿 |
int64,uint64 |
8 |
0 |
|
float32 |
4 |
0.0 |
|
float64 |
8 |
0.0 |
默认浮点数类型 |
complex64 |
8 |
|
|
complex128 |
16 |
|
|
rune |
4 |
0 |
Unicode Code Point, int32 |
uintptr |
4,8 |
0 |
足以存储指针的 uint |
string |
|
"" |
字符串,默认值位空字符串,而非 NULL |
array |
|
|
数组 |
struct |
|
|
结构体 |
function |
|
nil |
函数 |
interface |
|
nil |
接口 |
map |
|
nil |
字典,引用类型 |
slice |
|
nil |
切片,引用类型 |
channel |
|
nil |
通道,引用类型 |
bool, string
(u)int, (u)int8, (u)int16, (u)int32, (u)int64, uintptr(指针)
byte, rune
float32, float64, complex64, complex128
没有 char 只有 rune
支持八进制、十六进制以及科学记数法。
标准库 math 定义了各数字类型的取值范围.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | package main
import (
"fmt"
"math"
)
func main() {
a, b, c := 100, 0144, 0x64
fmt.Println(a, b, c)
fmt.Printf("0b%b, %#o, %#x\n", a, a, a)
fmt.Println(math.MinInt8, math.MaxInt8)
}
|
输出:
| 100 100 100
0b1100100, 0144, 0x64
-128 127
|
标准库 strconv 可在不同进制(字符串)间转换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 | package main
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
|
使用浮点数时,须注意小数位的有效精度,相关细节可参考 IEEE-754 标准
1
2
3
4
5
6
7
8
9
10
11
12
13
14 | package main
import "fmt"
func main() {
var a float32 = 1.1234567899 // 注意: 默认浮点类型是 float64
var b float32 = 1.12345678
var c float32 = 1.123456781
println(a, b, c)
println(a == b, a == c)
fmt.Printf("%v%v, %v\n", a, b, c)
}
|
输出:
| +1.123457e+000 +1.123457e+000 +1.123457e+000
true true
1.12345681.1234568, 1.1234568
|
1
2
3
4
5
6
7
8
9
10
11
12 | package main
import (
"fmt"
"math/cmplx"
)
func main() {
a := 3 + 4i
fmt.Println(cmplx.Abs(a))
}
// 5
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 | package main
import (
"fmt"
"math"
"math/cmplx"
)
func euler() {
fmt.Println(cmplx.Exp(1i*math.Pi)+1,
cmplx.Pow(math.E, 1i*math.Pi)+1)
fmt.Printf("%.3f\n", cmplx.Exp(1i*math.Pi)+1)
}
func triangle() {
var a, b int = 3, 4
var c int
c = int(math.Sqrt(float64(a*a + b*b)))
fmt.Println(c)
}
func main() {
euler()
triangle()
}
// (0+1.2246467991473515e-16i) (0+1.2246467991473515e-16i)
// (0.000+0.000i)
// 5
|
别名
在官方的语言规范中,专门提到两个别名
| byte alias for uint8
rune alias for int32
|
别名类型无须转换,可直接赋值
1
2
3
4
5
6
7
8
9
10
11
12 | package main
func test(x byte) {
println(x)
}
func main() {
var a byte = 0x11
var b uint8 = a
var c uint8 = a + b
test(c)
}
|
但这并不表示,拥有相同底层结构的就属于别名。
就算在 64 位平台上 int 和 int64 结构完全一致,也分属不同类型,须显式转换
| func add(x, y int) int {
return x + y
}
func main() {
var x int = 100
var y int64 = x // 错误: cannot use x(type int) as type int64 in assignment
add(x, y) // 错误: cannot use y(type int64) as type int in argument to add
}
|
整型
尽管Go语言提供了无符号数和运算,即使数值本身不可能出现负数我们还是倾向于使用有符号的int类型,就像数组的长度那样,虽然使用uint无符号类型似乎是一个更合理的选择。
事实上,内置的len函数返回一个有符号的int,我们可以像下面例子那样处理逆序循环。
| medals := []string{"gold", "silver", "bronze"}
for i := len(medals) - 1; i >= 0; i-- {
fmt.Println(medals[i]) // "bronze", "silver", "gold"
}
|
另一个选择对于上面的例子来说将是灾难性的。如果len函数返回一个无符号数,那么i也将是无符号的uint类型,然后条件 i >= 0
则永远为真。
在三次迭代之后,也就是 i == 0
时,i--
语句将不会产生 -1
,而是变成一个uint类型的最大值,
然后 medals[i]
表达式将发生运行时panic异常,也就是试图访问一个slice范围以外的元素。
出于这个原因,无符号数往往只有在位运算或其它特殊的运算场景才会使用,就像bit集合、分析二进制文件格式或者是哈希和加密操作等。
当使用fmt包打印一个数值时,我们可以用 %d
、%o
或 %x
参数控制输出的进制格式,就像下面的例子:
| o := 0666
fmt.Printf("%d %[1]o %#[1]o\n", o) // "438 666 0666"
x := int64(0xdeadbeef)
fmt.Printf("%d %[1]x %#[1]x %#[1]X\n", x)
// Output:
// 3735928559 deadbeef 0xdeadbeef 0XDEADBEEF
|
请注意fmt的两个使用技巧。通常Printf格式化字符串包含多个 %
参数时将会包含对应相同数量的额外操作数,但是 %
之后的 [1]
副词告诉Printf函数再次使用第一个操作数。
第二,%
后的 #
副词告诉Printf在用 %o
、%x
或 %X
输出时生成 0
、0x
或 0X
前缀。
字符面值通过一对单引号直接包含对应字符。最简单的例子是ASCII中类似 'a'
写法的字符面值,但是我们也可以通过转义的数值来表示任意的Unicode码点对应的字符,马上将会看到这样的例子。
字符使用%c
参数打印,或者是用%q
参数打印带单引号的字符:
| ascii := 'a'
unicode := '国'
newline := '\n'
fmt.Printf("%d %[1]c %[1]q\n", ascii) // "97 a 'a'"
fmt.Printf("%d %[1]c %[1]q\n", unicode) // "22269 国 '国'"
fmt.Printf("%d %[1]q\n", newline) // "10 '\n'"
|
浮点数
Go语言提供了两种精度的浮点数,float32和float64。它们的算术规范由IEEE754浮点数国际标准定义,该浮点数规范被所有现代的CPU支持。
这些浮点数类型的取值范围可以从很微小到很巨大。浮点数的范围极限值可以在math包找到。
常量math.MaxFloat32表示float32能表示的最大数值,大约是 3.4e38
;对应的 math.MaxFloat64
常量大约是 1.8e308
。
它们分别能表示的最小值近似为 1.4e-45
和 4.9e-324
。
一个float32类型的浮点数可以提供大约6个十进制数的精度,而float64则可以提供约15个十进制数的精度;
通常应该优先使用float64类型,因为float32类型的累计计算误差很容易扩散,并且float32能精确表示的正整数并不是很大
(因为float32的有效bit位只有23个,其它的bit位用于指数和符号;当整数大于23bit能表达的范围时,float32的表示将出现误差):
| var f float32 = 16777216 // 1 << 24
fmt.Println(f == f+1) // "true"!
|
用Printf函数的 %g
参数打印浮点数,将采用更紧凑的表示形式打印,并提供足够的精度,但是对应表格的数据,使用 %e
(带指数)或 %f
的形式打印可能更合适。
所有的这三个打印形式都可以指定打印的宽度和控制打印精度。
| for x := 0; x < 8; x++ {
fmt.Printf("x = %d e^x = %8.3f\n", x, math.Exp(float64(x)))
}
|
上面代码打印e的幂,打印精度是小数点后三个小数精度和8个字符宽度:
| x = 0 e^x = 1.000
x = 1 e^x = 2.718
x = 2 e^x = 7.389
x = 3 e^x = 20.086
x = 4 e^x = 54.598
x = 5 e^x = 148.413
x = 6 e^x = 403.429
x = 7 e^x = 1096.633
|
math包中除了提供大量常用的数学函数外,还提供了IEEE754浮点数标准中定义的特殊值的创建和测试:正无穷大和负无穷大,分别用于表示太大溢出的数字和除零的结果;
还有NaN非数,一般用于表示无效的除法操作结果 0/0
或 Sqrt(-1)
.
| var z float64
fmt.Println(z, -z, 1/z, -1/z, z/z) // "0 -0 +Inf -Inf NaN"
|
函数 math.IsNaN
用于测试一个数是否是非数 NaN,math.NaN
则返回非数对应的值。
虽然可以用 math.NaN
来表示一个非法的结果,但是测试一个结果是否是非数 NaN 则是充满风险的,因为NaN和任何数都是不相等的(在浮点数中,NaN、正无穷大和负无穷大都不是唯一的,每个都有非常多种的bit模式表示):
| nan := math.NaN()
fmt.Println(nan == nan, nan < nan, nan > nan) // "false false false"
|
如果一个函数返回的浮点数结果可能失败,最好的做法是用单独的标志报告失败,像这样:
| func compute() (value float64, ok bool) {
// ...
if failed {
return 0, false
}
return result, true
}
|
引用类型
所谓引用类型特指 slice、map、channel 这三种预定义类型
相比数字、数组等类型,引用类型拥有更复杂的存储结构。
除分配内存外,它们还须初始化一系列属性,诸如指针、长度,甚至包括哈希分布、数据队列等。
内置函数 new 按指定类型长度分配零值内存,返回指针,并不关心类型内部构造和初始化方式。
而引用类型则必须使用 make 函数创建,编译器会将 make 转换为目标类型专用的创建函数(或指令),以确保完成全部内存分配和相关属性初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 | package main
func mkslice() []int {
s := make([]int, 0, 10)
s = append(s, 100)
return s
}
func mkmap() map[string]int {
m := make(map[string]int)
m["a"] = 1
return m
}
func main() {
m := mkmap()
println(m["a"])
s := mkslice()
println(s[0])
}
|
除 new/make 函数外,也可使用初始化表达式,编译器生成的指令基本相同
当然,new 函数也可为引用类型分配内存,但这是不完整创建。
以字典(map)为例,它仅为分配了字典类型本身(实际就是个指针包装)所需内存,并没有分配键值存储内存,也没有初始化散列桶等内部属性,因此它无法正常工作
| package main
import "fmt"
func main() {
p := new(map[string]int) // 函数 new 返回指针
m := *p
m["a"] = 1 // panic:assignment to entry in nil map(运行期错误)
fmt.Println(m)
}
|
类型转换
隐式转换造成的问题远大于它带来的好处。
除常量、别名类型以及未命名类型外,Go 强制要求使用显式类型转换。
加上不支持操作符重载,所以我们总是能确定语句及表达式的明确含义。
| a := 10
b := byte(a)
c := a + int(b) // 混合类型表达式必须确保类型一致
|
同样不能将非 bool 类型结果当作 ture/false 使用
| func main() {
x := 100
var b bool = x // 错误: cannot use x(type int) as type bool in assignment
if x { // 错误: non-bool x(type int)used as if condition
}
}
|
语法歧义
如果转换的目标是指针、单向通道或没有返回值的函数类型,那么必须使用括号,以避免造成语法分解错误。
| func main() {
x := 100
p := *int(&x) // 错误: cannot convert &x(type *int) to type int
// invalid indirect of int(&x) (type int)
println(p)
}
|
正确的做法是用括号,让编译器将*int
解析为指针类型
| (*int)(p) -> 如果没有括号 --> *(int(p))
(<-chan int)(c) <-(chan int(c))
(func())(x) func()x
func() int (x) -> 有返回值的函数类型可省略括号,但仍然建议使用
(func() int) (x) 使用括号后,更易阅读
|
自定义类型
使用关键字 type 定义用户自定义类型,包括基于现有基础类型创建,或者是结构体、函数类型等
1
2
3
4
5
6
7
8
9
10
11
12 | type flags byte
const (
read flags = 1 << iota
write
exec
)
func main() {
f := read | exec
fmt.Printf("%b\n", f) // 输出二进制标记位
}
|
输出:
和 var、const 类似,多个 type 定义可合并成组,可在函数或代码块内定义局部类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 | func main() {
type( // 组
user struct { // 结构体
name string
age uint8
}
event func(string) bool // 函数类型
)
u := user{"Tom", 20}
fmt.Println(u)
var f event = func(s string) bool {
println(s)
return s != ""
}
f("abc")
}
|
即便指定了基础类型,也只表明它们有相同底层数据结构,两者间不存在任何关系,属完全不同的两种类型。
除操作符外,自定义类型不会继承基础类型的其他信息(包括方法)。
不能视作别名,不能隐式转换,不能直接用于比较表达式
| func main() {
type data int
var d data = 10
var x int = d // 错误: cannot use d(type data) as type int in assignment
println(x)
println(d == x) // 错误: invalid operation: d == x(mismatched types data and int)
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | // Package tempconv performs Celsius and Fahrenheit temperature computations.
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) }
|
我们在这个包声明了两种类型:Celsius和Fahrenheit分别对应不同的温度单位。
它们虽然有着相同的底层类型float64,但是它们是不同的数据类型,因此它们不可以被相互比较或混在一个表达式运算。
刻意区分类型,可以避免一些像无意中使用不同单位的温度混合计算导致的错误;因此需要一个类似Celsius(t)或Fahrenheit(t)形式的显式转型操作才能将float64转为对应的类型。
Celsius(t)
和 Fahrenheit(t)
是类型转换操作,它们并不是函数调用。类型转换不会改变值本身,但是会使它们的语义发生变化。
另一方面,CToF和FToC两个函数则是对不同温度单位下的温度进行换算,它们会返回不同的值。
对于每一个类型T,都有一个对应的类型转换操作 T(x)
,用于将x转为T类型(如果T是指针类型,可能会需要用小括弧包装 T,比如 (*int)(0)
)。
只有当两个类型的底层基础类型相同时,才允许这种转型操作,或者是两者都是指向相同底层结构的指针类型,这些转换只改变类型而不会影响值本身。
如果x是可以赋值给T类型的值,那么x必然也可以被转为T类型,但是一般没有这个必要。
底层数据类型决定了内部结构和表达方式,也决定是否可以像底层类型一样对内置运算符的支持。
这意味着,Celsius和Fahrenheit类型的算术运算行为和底层的float64类型是一样的,正如我们所期望的那样。
| fmt.Printf("%g\n", BoilingC-FreezingC) // "100" °C
boilingF := CToF(BoilingC)
fmt.Printf("%g\n", boilingF-CToF(FreezingC)) // "180" °F
fmt.Printf("%g\n", boilingF-FreezingC) // compile error: type mismatch
|
比较运算符 ==
和 <
也可以用来比较一个命名类型的变量和另一个有相同类型的变量,或有着相同底层类型的未命名类型的值之间做比较。
但是如果两个值有着不同的类型,则不能直接进行比较:
| var c Celsius
var f Fahrenheit
fmt.Println(c == 0) // "true"
fmt.Println(f >= 0) // "true"
fmt.Println(c == f) // compile error: type mismatch
fmt.Println(c == Celsius(f)) // "true"!
|
注意最后那个语句。尽管看起来像函数调用,但是 Celsius(f)
是类型转换操作,它并不会改变值,仅仅是改变值的类型而已。
测试为真的原因是因为c和g都是零值。
一个命名的类型可以提供书写方便,特别是可以避免一遍又一遍地书写复杂类型(例如用匿名的结构体定义变量)。
虽然对于像float64这种简单的底层类型没有简洁很多,但是如果是复杂的类型将会简洁很多,特别是结构体类型。
下面的声明语句,Celsius类型的参数c出现在了函数名的前面,表示声明的是Celsius类型的一个叫名叫String的方法,该方法返回该类型对象c带着 °C
温度单位的字符串:
| func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }
|
许多类型都会定义一个String方法,因为当使用fmt包的打印方法时,将会优先使用该类型对应的String方法返回的结果打印
| c := FToC(212.0)
fmt.Println(c.String()) // "100°C"
fmt.Printf("%v\n", c) // "100°C"; no need to call String explicitly
fmt.Printf("%s\n", c) // "100°C"
fmt.Println(c) // "100°C"
fmt.Printf("%g\n", c) // "100"; does not call String
fmt.Println(float64(c)) // "100"; does not call String
|
未命名类型
与有明确标识符的 bool、int、string 等类型相比,数组、切片、字典、通道等类型与具体元素类型或长度等属性有关,故称作未命名类型。
当然,可用 type 为其提供具体名称,将其改编为命名类型。
具有相同声明的未命名类型被视作同一类型
- 具有相同基类型的指针
- 具有相同元素类型和长度的数组(array)
- 具有相同元素类型的切片(slice)
- 具有相同键值类型的字典(map)
- 具有相同数据类型及操作方法的通道(channel)
- 具有相同字段序列(字段名、字段类型、标签,以及字段顺序)的结构体(struct)
- 具有相同签名(参数和返回值列表,不包括参数名)的函数(func)
- 具有相同方法集(方法名、方法签名,不补哦阔顺序)的接口(interface)
容易被忽略的是 struct tag,它也属于类型组成部分,而不仅仅是元数据描述
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | func main() {
var a struct { // 匿名结构类型
x int `x`
s string `s`
}
var b struct {
x int
s string
}
b = a // 错误: cannot use a type struct{x int "x"; s string "s"} as type
// struct{x int; s string} in assignment
fmt.Println(b)
}
|
同样,函数的参数顺序也属签名组成部分
| func main() {
var a func(int, string)
var b func(string, int)
b = a // 错误: cannot use a(type func(int, string)) as type
// func(string, int) in assignment b("s", 1)
}
|
未命名类型转换规则:
- 所属类型相同
- 基础类型相同,且其中一个是未命名类型
- 数据类型相同,将双向通道赋值给单向通道,且其中一个为未命名类型
- 将默认值 nil 赋值给切片、字典、通道、指针、函数或接口
- 对象实现了目标接口
| func main() {
type data[2] int
var d data = [2]int{1, 2} // 基础类型相同,右值为未命名类型
fmt.Println(d)
a := make(chan int, 2)
var b chan <-int = a // 双向通道转换为单向通道,其中 b 为未命名乐境
b <- 2
}
|
条件语句
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35 | package main
import (
"fmt"
"io/ioutil"
)
func bounded(v int) int {
if v > 100 {
return 100
} else if v < 0 {
return 0
} else {
return v
}
}
func main() {
fmt.Println(bounded(100))
const filename = "abc.txt"
// contents, err := ioutil.ReadFile(filename)
// if err != nil {
// fmt.Println(err)
// } else {
// fmt.Printf("%s\n", contents)
// }
if contents, err := ioutil.ReadFile(filename); err != nil {
fmt.Println(err)
} else {
fmt.Printf("%s\n", contents)
}
}
// 100
// 1 2 3
// hhh
|
if 的条件里可以赋值
if 的条件里赋值的变量作用域就在这个 if 语句里
函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76 | package main
import (
"fmt"
"math"
"reflect"
"runtime"
)
func eval(a, b int, op string) (int, error) {
switch op {
case "+":
return a + b, nil
case "-":
return a - b, nil
case "*":
return a * b, nil
case "/":
q, _ := div(a, b)
return q, nil
default:
return 0, fmt.Errorf(
"unsupported operation: %s", op)
}
}
func div(a, b int) (q, r int) {
// return a / b, a % b
q = a / b
r = a % b
return
}
func apply(op func(int, int) int, a, b int) int {
p := reflect.ValueOf(op).Pointer()
opName := runtime.FuncForPC(p).Name()
fmt.Printf("Calling function %s with args "+
"(%d %d)\n", opName, a, b)
return op(a, b)
}
func pow(a, b int) int {
return int(math.Pow(float64(a), float64(b)))
}
func sum(numbers ...int) int {
s := 0
for i := range numbers {
s += numbers[i]
}
return s
}
func main() {
fmt.Println(eval(3, 4, "/"))
a, b := div(13, 4)
fmt.Println(a, b)
if result, err := eval(3, 4, "h"); err != nil {
fmt.Println(err)
} else {
fmt.Println(result)
}
fmt.Println(apply(pow, 4, 3))
fmt.Println(apply(func(a, b int) int {
return int(math.Pow(float64(a), float64(b)))
}, 3, 4))
fmt.Println(1, 2, 3, 4, 5)
}
// 0 <nil>
// 3 1
// unsupported operation: h
// Calling function main.pow with args (4 3)
// 64
// Calling function main.main.func1 with args (3 4)
// 81
// 1 2 3 4 5
|
指针
| package main
import "fmt"
func main() {
var a int = 2
var pa *int = &a
*pa = 3
fmt.Println(a)
}
// 3
|
指针不能运算
Go 语言只有值传递一种方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 | package main
import "fmt"
// func swap(a, b *int) {
// *b, *a = *a, *b
// }
func swap(a, b int) (int, int) {
return b, a
}
func main() {
a, b := 3, 4
// swap(&a, &b)
a, b = swap(a, b)
fmt.Println(a, b)
}
// 4 3
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 | package main
import "fmt"
func zeroval(ival int) {
ival = 0
}
func zeroptr(iptr *int) {
*iptr = 0
}
func main() {
i := 1
fmt.Println("initial:", i)
zeroval(i)
fmt.Println("zeroval:", i)
zeroptr(&i)
fmt.Println("zeroptr:", i)
fmt.Println("pointer:", &i)
}
|
输出结果:
| initial: 1
zeroval: 1
zeroptr: 0
pointer: 0xc00002a0f8
|
随机数
1
2
3
4
5
6
7
8
9
10
11
12 | package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
rand.Seed(time.Now().UnixNano())
fmt.Println(rand.Intn(100))
}
|