Go 语言基础语法快速入门
1. 简介
Go 语言是静态类型语言,因此变量(variable)是有明确类型的,编译器也会检查变量类型的正确性。从计算机系统实现角度来看,变量是一段或多段用来存储数据的内存。
2. 变量 (Variables)
2.1. 变量声明
Go 语言提供了多种变量声明的方式。
标准格式
var 是声明变量的关键字,标准格式如下:
1
var 变量名 变量类型
批量格式
使用关键字 var 和括号,可以将一组变量定义放在一起。
1
2
3
4
5
6
7
8
9
var (
a int
b string
c []float32
d func() bool
e struct {
x int
}
)
简短格式 (Short Variable Declaration)
在函数内部,可以使用 := 操作符进行简短的变量声明和初始化。
1
名字 := 表达式
例如:
1
2
3
4
func main() {
x := 100
a, s := 1, "abc"
}
注意:简短格式有以下限制:
- 定义变量的同时必须显式初始化。
- 不能提供数据类型,由编译器根据右值自动推导。
- 只能用在函数内部。
2.2. 变量初始化
Go 语言在声明变量时,会自动对变量对应的内存区域进行初始化。每个变量会初始化为其类型的零值(Zero Value)。
- 整型 默认为
0 - 浮点型 默认为
0.0 - 布尔型 默认为
false - 字符串 默认为
""(空字符串) - 切片, 函数, 指针, 接口, Map, Channel 默认为
nil
显式初始化
可以在声明变量时提供一个初始值。
1
2
3
4
5
6
7
8
9
// 标准格式初始化
var hp int = 100
// 编译器推导类型
// 由于 100 是整型,Go 会推导出 hp 的类型为 int
var hp = 100
// 短变量声明并初始化
hp := 100
注意:使用
:=时,左侧的变量必须至少有一个是新声明的。否则会导致编译错误。
1
2
3
4
5
6
7
// 正确:conn2 是新变量,err 是重复声明
conn, err := net.Dial("tcp", "127.0.0.1:8080")
conn2, err := net.Dial("tcp", "127.0.0.1:8080")
// 错误:hp 已经被声明过
var hp int
hp := 100 // no new variables on left side of :=
2.3. 多变量赋值
Go 支持多重赋值,这个特性可以被巧妙地用来实现变量交换。
1
2
3
4
5
var a int = 100
var b int = 200
// 交换 a 和 b 的值
a, b = b, a
2.4. 匿名变量
匿名变量是一个下划线 _,被称为空白标识符。任何赋给它的值都将被抛弃,它不占用内存空间,也不会分配内存。这在调用多返回值的函数,但又不需要所有返回值时非常有用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 函数 getData 返回两个 int 类型的值
func getData() (int, int) {
return 100, 200
}
func main() {
// 只获取第一个返回值, 第二个抛弃
a, _ := getData()
// 只获取第二个返回值, 第一个抛弃
_, b := getData()
fmt.Println(a, b)
}
2.5. 变量作用域
根据变量定义位置的不同,可分为三类:
- 局部变量: 函数内定义的变量。其作用域仅在函数体内,调用结束后即销毁。
- 全局变量: 函数外定义的变量。可在整个包甚至外部包(如果首字母大写)中使用。
- 形式参数: 函数定义中的变量,作为函数的局部变量使用。
如果全局变量和局部变量名称相同,在函数体内,局部变量会被优先考虑。
3. 常量 (Constants)
在 Go 语言中,使用 const 关键字定义常量。常量的值在编译时就已经确定,无法在运行时修改。
1
2
3
4
5
const Pi = 3.14159
const Avogadro = 6.02214129e23 // 阿伏伽德罗常数
// 常量也可以不指定类型,由编译器自动推导
const World = "世界"
4. 基本数据类型
4.1. 布尔类型 (bool)
布尔类型的值只有 true 和 false 两种。
- 支持
&&(逻辑与) 和||(逻辑或) 运算,且具有短路行为(如果左侧的值已经能决定整个表达式的结果,右侧将不再被求值)。 - 布尔型与整型之间不能进行隐式或显式转换。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
s := ""
// 因为短路行为,即使 s 为空,s[0] 也不会被执行,避免了 panic
if s != "" && s[0] == 'x' {
// ...
}
// 如果需要转换,必须显式进行
func btoi(b bool) int {
if b {
return 1
}
return 0
}
func itob(i int) bool {
return i != 0
}
4.2. 数值类型
整型 (Integer)
Go 语言提供了多种有符号和无符号的整数类型:
- 有符号:
int8,int16,int32,int64 - 无符号:
uint8,uint16,uint32,uint64
此外,int 和 uint 的大小取决于目标平台的机器字长(32位或64位)。
两个特殊的别名:
byte是uint8的别名,常用于强调原始数据。rune是int32的别名,常用于表示一个 Unicode 码点。
uintptr 是一种特殊的无符号整数,大小足以容纳一个指针,主要用于底层编程。
浮点型 (Float)
Go 提供 float32 和 float64 两种精度的浮点数。
float32提供约 6 位十进制精度。float64提供约 15 位十进制精度,是默认和推荐使用的类型。
1
2
3
4
5
6
7
8
9
10
// float32 的精度问题
var f float32 = 16777216 // 1 << 24
fmt.Println(f == f+1) // 输出 "true"
// 科学计数法
const Planck = 6.62606957e-34
// 格式化输出
fmt.Printf("%f\n", math.Pi) // 默认精度
fmt.Printf("%.2f\n", math.Pi) // 保留两位小数
复数 (Complex)
Go 支持复数,类型为 complex64 和 complex128(默认)。
1
2
3
4
5
6
// 声明一个复数 1+2i
var x complex128 = complex(1, 2)
// 获取实部和虚部
fmt.Println(real(x)) // 输出 1
fmt.Println(imag(x)) // 输出 2
4.3. 字符串类型 (string)
字符串是不可改变的字节序列,内部采用 UTF-8 编码。
1
2
3
4
5
6
7
8
9
10
// 字符串拼接
s1 := "hello, "
s2 := "world"
s3 := s1 + s2 // "hello, world"
// 获取长度(字节数)
len(s3) // 12
// 访问单个字节 (只对 ASCII 有效)
char := s3[0] // 值为 'h' 的 byte
重要:获取字符串中某个字节的地址是非法行为,例如
&str[i]。
多行字符串
使用反引号 ` 定义多行字符串。在反引号中,所有转义字符都无效,内容会原样输出。
1
2
3
4
const str = `第一行
第二行
第三行 \r\n`
fmt.Println(str)
4.4. 字符类型 (byte 和 rune)
Go 语言的字符有两种类型:
byte:uint8的别名,代表一个 ASCII 字符。rune:int32的别名,代表一个 UTF-8 字符,用于处理多字节字符(如中文)。
1
2
3
4
var ch_ascii byte = 'A'
var ch_utf8 rune = '你'
fmt.Printf("ASCII: %c, UTF-8: %c\n", ch_ascii, ch_utf8)
UTF-8 与 Unicode 的区别
- Unicode 是一个字符集,为世界上每个字符分配了一个唯一的数字 ID(码点)。
- UTF-8 是一种编码规则,它定义了如何将 Unicode 码点存储为字节序列。UTF-8 是一种变长编码,ASCII 字符占用1字节,而像汉字这样的字符通常占用3字节。
5. 类型系统
5.1. 数据类型转换
Go 语言没有隐式类型转换,所有类型转换都必须显式声明。
1
2
3
// 格式:目标类型(待转化的值)
var a int32 = 100
var b int64 = int64(a)
- 只有底层类型相同的变量才能相互转换。
- 从大范围类型向小范围类型转换可能会导致精度丢失或数值截断。
1
2
3
4
5
6
7
8
// 浮点数转整数,小数部分会被截断
var c float32 = 3.14
fmt.Println(int(c)) // 输出 3
// 大整数转小整数,高位被截断
var a int32 = 1047483647 // 0x3e6f54ff
b := int16(a) // 0x54ff,值为 21759
fmt.Println(b)
5.2. 自定义类型 (type)
type 关键字有两种用途:定义一个全新的类型,或创建一个类型别名。
类型定义
type NewType BaseType
这会创建一个全新的类型 NewType,它和 BaseType 不是同一个类型,不能直接通用,但可以相互转换。
1
2
3
4
type NewInt int
var a NewInt
// a = 10 // 错误:不能将 int 直接赋值给 NewInt
a = NewInt(10) // 正确
类型别名 (Go 1.9+)
type TypeAlias = BaseType
这只是为 BaseType 创建了一个别名 TypeAlias,两者在本质上是完全相同的类型。
1
2
3
type IntAlias = int
var a IntAlias
a = 10 // 正确,因为 IntAlias 和 int 是同一个类型
注意:不能为一个非本地类型(即在其他包中定义的类型)的别名定义新方法。
6. 运算符
下表展示了 Go 语言中运算符的优先级,优先级值越大,表示优先级越高。
| 优先级 | 分类 | 运算符 | 结合性 |
|---|---|---|---|
| 14 | 后缀/选择器 | () [] . | 从左到右 |
| 13 | 单目运算符 | + - ! ^ * & <- | 从右到左 |
| 12 | 乘法/除法等 | * / % << >> & &^ | 从左到右 |
| 11 | 加法/减法等 | + - | ^ | 从左到右 |
| 9 | 关系运算符 | == != < <= > >= | 从左到右 |
| 4 | 逻辑与 | && | 从左到右 |
| 3 | 逻辑或 | || | 从左到右 |
如果不确定优先级,使用括号
()可以明确指定运算顺序,因为括号的优先级最高。 注意: 在多个短变量声明和赋值中, 至少有一个新声明的变量出现在左值中, 即使其他变量名可能是重复声明的, 编译器也不会报错, 如:
conn, err := net.Dial(“tcp”,“127.0.0.1:8080”) // 编译器不会报err重复定义 conn2, err := net.Dial(“tcp”,“127.0.0.1:8080”)
多个变量同时赋值
变量交换算法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func swap1 {
var a int =100
var b int = 200
var temp int temp = a
a = b
b = temp
}
func swap2 {
var a int = 100
var b int = 200
a = a^b
b = b^a
a = a^b
}
利用Go的多重赋值特性, 实现变量交换任务
1
2
3
var a int = 100
var b int = 200
a, b = b, a
多重赋值时, 变量的左值和右值按从左到右的顺序赋值
匿名变量
匿名变量的特点是一个下划线”“,””本身就是一个特殊的标识符, 被称为空白标识符. 它可以像其他标识符那样用于变量的声明或赋值(), 但任何赋给这个标识符的值都将被抛弃, 因此这些值不能在后续的代码中使用, 也不可以使用这个标识符作为变量对其他变量进行赋值或运算. 使用匿名变量时, 只需要在变量声明的地方使用下划线替换即可. 例如
// 函数名getData, 返回值 (int, int) func getData() (int, int) { return 100,200 } func main() { // 只获取第一个返回值, 第二个返回值设为匿名变量 a, _ := getData() // 将第一个返回值设为匿名变量 _, b := getData() fmt.Println(a, b) }
匿名变量不占用内存空间, 不会分配内存. 匿名变量与匿名变量之间也不会因为多次声明而无法使用
变量的作用域
和其他语言类似, 根据变量定义位置的不同, 分为三类:
- 局部变量: 函数内定义的变量
- 全局变量: 函数外定义的变量
- 形式参数: 函数定义中的变量
局部变量
局部变量的作用域只在函数体内, 函数的参数和返回值变量都属于局部变量.
局部变量不是一直存在的, 它只在定义它的函数被调用后存在, 函数调用结束后这个局部变量就会被销毁.
全局变量
在函数体外声明的变量称为全局变量, 全局变量只需要在一个源文件中定义, 就可以在所有源文件中使用, 当然, 不包括这个全局变量的源文件需要使用 “import” 关键字引入全局变量所在的源文件之后才能使用这个全局变量.
全局变量声明必须以 var 关键字开头, 如果想要在外部包中使用,则全局变量的首字母必须大写
全局变量和局部变量名称可以相同, 但在函数体内 局部变量会被优先考虑
形式参数
在定义函数时函数名后面括号中的变量叫做形式参数, 形式参数只有在函数调用时才会生效, 函数调用结束后就会被销毁, 在函数未被调用时, 函数的形参并不占用实际的存储单元, 也没有实际值.
形式参数会作为函数的局部变量来使用.
Go语言基本类型
整形
整形分为有符号整形和无符号整形
Go语言同时提供了有符号和无符号的整数类型,其中包括 int8、int16、int32 和 int64 四种大小截然不同的有符号整数类型,分别对应 8、16、32、64 bit(二进制位)大小的有符号整数,与此对应的是 uint8、uint16、uint32 和 uint64 四种无符号整数类型。
此外还有两种整数类型 int 和 uint,它们分别对应特定 CPU 平台的字长(机器字大小),其中 int 表示有符号整数,应用最为广泛,uint 表示无符号整数。实际开发中由于编译器和计算机硬件的不同,int 和 uint 所能表示的整数大小会在 32bit 或 64bit 之间变化。
用来表示 Unicode 字符的 rune 类型和 int32 类型是等价的,通常用于表示一个 Unicode 码点。这两个名称可以互换使用。同样,byte 和 uint8 也是等价类型,byte 类型一般用于强调数值是一个原始的数据而不是一个小的整数。
最后,还有一种无符号的整数类型 uintptr,它没有指定具体的 bit 大小但是足以容纳指针。uintptr 类型只有在底层编程时才需要,特别是Go语言和C语言函数库或操作系统接口相交互的地方。
哪些情况下使用int和uint
程序逻辑对整形范围没有特殊要求.
如: 对象长度使用内建的len()函数返回, 这个长度可以根据不同平台的字节长度进行变化. 实际使用中, 切片或map的元素数量等都可以用int来表示
反之, 在二进制传输,读写文件的结构描述时, 为了保持文件的结构不会受到不同编译目标平台字节长度的影响, 不要使用int 和 uint
浮点型
Go语言提供了两种精度的浮点数 float32 和 float64,它们的算术规范由 IEEE754 浮点数国际标准定义,该浮点数规范被所有现代的 CPU 支持。
一个 float32 类型的浮点数可以提供大约 6 个十进制数的精度,而 float64 则可以提供约 15 个十进制数的精度,通常应该优先使用 float64 类型,因为 float32 类型的累计计算误差很容易扩散,并且 float32 能精确表示的正整数并不是很大。
var f float32 = 16777216 // 1 « 24 fmt.Println(f == f+1) // “true”!
浮点数在声明的时候可以只写整数部分或者小数部分,像下面这样:
const e = .5154 // 0.5154 const f = 1. // 1
很小或很大的数最好用科学计数法书写,通过 e 或 E 来指定指数部分:
const Avogadro = 6.02214129e23 // 阿伏伽德罗常数 const Planck = 6.62606957e-34 // 普朗克常数
用 Printf 函数打印浮点数时可以使用“%f”来控制保留几位小数
func main() { fmt.Printf(“%f”, math.Pi) fmt.Printf(“%.2f”, math.Pi) }
复数
在计算机中,复数是由两个浮点数表示的,其中一个表示实部(real),一个表示虚部(imag)。
Go语言中复数的类型有两种,分别是 complex128(64 位实数和虚数)和 complex64(32 位实数和虚数),其中 complex128 为复数的默认类型 。
复数的值由三部分组成 RE + IMi,其中 RE 是实数部分,IM 是虚数部分,RE 和 IM 均为 float 类型,而最后的 i 是虚数单位。
声明复数的语法格式如下所示:
var name complex128 = complex(x, y) // 简写为以下形式 name := complex(x, y)
其中 name 为复数的变量名,complex128 为复数的类型,“=”后面的 complex 为Go语言的内置函数用于为复数赋值,x、y 分别表示构成该复数的两个 float64 类型的数值,x 为实部,y 为虚部。
对于一个复数, 可以通过Go的内置函数 real(z) 来获取该复数的实部, 也就是x; 通过 imag(z) 获得该复数的虚部, 也就是 y.
例如: 使用 complex 函数构建复数, 并使用 real 和 imag 函数返回复数的实部和虚部:
var x complex128 = complex(1, 2)// 1+2i var x complex128 = complex(3, 4)// 3+4i fmt.Println(xy) // -5+10i fmt.Println(real(xy)) // -5 fmt.Println(imag(x*y)) // 10
复数也可以用 == 和 != 进行相等比较,只有两个复数的实部和虚部都相等的时候它们才是相等的。
bool类型
一个布尔类型的值只有两种: true 或 false
Go语言对于值之间的比较有非常严格的限制, 只有两个相同类型的值才可以进行比较, 如果值的类型是接口(interface), 那么他们也必须都实现了相同的接口. 如果其中一个值是常量, 那么另外一个值可以不是常量, 但是类型必须和该常量类型相同. 如果以上条件都不满足, 则必须将其中一个值的类型转换为和另外一个值的类型相同之后才可以进行比较.
| 布尔值可以和 && , | 操作符结合, 并且有短路行为, 如果运算符左边的值已经可以确定整个布尔表达式的值, 那么运算符右边的值将不再被求值, 因此下面的表达式总是安全的 |
// 如果s是个空字符串, s[0]会导致 panic 异常. s != “” && s[0] == ‘x’
| && 的优先级高于 |
布尔值并不会隐式转换为数字值 0 或 1,反之亦然,必须使用 if 语句显式的进行转换:
i := 0 if b { i = 1 }
如果需要经常做类似的转换,可以将转换的代码封装成一个函数,如下所示:
// 如果b为真,btoi返回1;如果为假,btoi返回0 func btoi(b bool) int { if b { return 1 } return 0 }
数字到布尔型的逆转换非常简单,不过为了保持对称,我们也可以封装一个函数:
// itob报告是否为非零。 func itob(i int) bool { return i != 0 }
Go语言中不允许将整型强制转换为布尔型,代码如下:
var n bool fmt.Println(int(n) * 2)
编译错误,输出如下:
cannot convert n (type bool) to type int
布尔型无法参与数值运算,也无法与其他类型进行转换。
字符串
一个字符串是一个不可改变的字节序列,字符串可以包含任意的数据,但是通常是用来包含可读的文本,字符串是 UTF-8 字符的一个序列(当字符为 ASCII 码表上的字符时则占用 1 个字节,其它字符根据需要占用 2-4 个字节)。
UTF-8 是一种被广泛使用的编码格式,是文本文件的标准编码,其中包括 XML 和 JSON 在内也都使用该编码。由于该编码对占用字节长度的不定性,在Go语言中字符串也可能根据需要占用 1 至 4 个字节,这与其它编程语言如 C++、Java 或者 Python 不同(Java 始终使用 2 个字节)。Go语言这样做不仅减少了内存和硬盘空间占用,同时也不用像其它语言那样需要对使用 UTF-8 字符集的文本进行编码和解码。
字符串是一种值类型,且值不可变,即创建某个文本后将无法再次修改这个文本的内容,更深入地讲,字符串是字节的定长数组。
定义字符串
可以使用双引号““ 来定义字符串,字符串中可以使用转义字符来实现换行、缩进等效果,常用的转义字符包括:
- :换行符
- 回车符
- tab 键
- Unicode 字符
- \:反斜杠自身
一般的比较运算符(==、!=、<、<=、>=、>)是通过在内存中按字节比较来实现字符串比较的,因此比较的结果是字符串自然编码的顺序。字符串所占的字节长度可以通过函数 len() 来获取,例如 len(str)。
字符串的内容(纯字节)可以通过标准索引法来获取,在方括号[ ] 内写入索引,索引从 0 开始计数:
- 字符串 str 的第 1 个字节:str[0]
- 第 i 个字节:str[i - 1]
- 最后 1 个字节:str[len(str)-1]
需要注意的是,这种转换方案只对纯 ASCII 码的字符串有效。
注意:获取字符串中某个字节的地址属于非法行为,例如 &str[i]。
字符串拼接符”+”
两个字符串 s1 和 s2 可以通过 s := s1 + s2 拼接在一起。将 s2 追加到 s1 尾部并生成一个新的字符串 s。
可以通过下面的方式来对代码中多行的字符串进行拼接:
str := “Beginning of the string” + “second part of the string”
提示:因为编译器会在行尾自动补全分号,所以拼接字符串用的加号“+”必须放在第一行末尾。
字符串实现基于UTF-8编码
Go语言中字符串的内部实现使用 UTF-8 编码,通过 rune 类型,可以方便地对每个 UTF-8 字符进行访问。当然,Go语言也支持按照传统的 ASCII 码方式逐字符进行访问。
定义多行字符串
在Go语言中,使用双引号书写字符串的方式是字符串常见表达方式之一,被称为字符串字面量(string literal),这种双引号字面量不能跨行,如果想要在源码中嵌入一个多行字符串时,就必须使用 ` 反引号,代码如下:
const str = 第一行 第二行 第三行 \r\n fmt.Println(str)
代码运行结果:
第一行 第二行 第三行
在这种方式下,反引号间换行将被作为字符串中的换行,但是所有的转义字符均无效,文本将会原样输出。
字符类型(byte和rune)
字符串中的每一个元素叫做“字符”,在遍历或者单个获取字符串元素时可以获得字符。
Go语言的字符有以下两种:
- 一种是 uint8 类型,或者叫 byte 型,代表了 ASCII 码的一个字符。
- 另一种是 rune 类型,代表一个 UTF-8 字符,当需要处理中文、日文或者其他复合字符时,则需要用到 rune 类型。rune 类型等价于 int32 类型。
byte 类型是 uint8 的别名,对于只占用 1 个字节的传统 ASCII 编码的字符来说,完全没有问题,例如 var ch byte = ‘A’,字符使用单引号括起来。
Go语言同样支持 Unicode(UTF-8),因此字符同样称为 Unicode 代码点或者 runes,并在内存中使用 int 来表示。在文档中,一般使用格式 U+hhhh 来表示,其中 h 表示一个 16 进制数。
在书写 Unicode 字符时,需要在 16 进制数之前加上前缀 /font> 或者 /font> 。因为 Unicode 至少占用 2 个字节,所以我们使用 int16 或者 int 类型来表示。如果需要使用到 4 字节,则使用 /font> 前缀,如果需要使用到 8 个字节,则使用 /font> 前缀。
var ch int = ‘041’ var ch2 int = ‘3B2’ var ch3 int = ‘0101234’ fmt.Printf(“%d - %d - %d”, ch, ch2, ch3) // integer fmt.Printf(“%c - %c - %c”, ch, ch2, ch3) // character fmt.Printf(“%X - %X - %X”, ch, ch2, ch3) // UTF-8 bytes fmt.Printf(“%U - %U - %U”, ch, ch2, ch3) // UTF-8 code point
输出:
65 - 946 - 1053236 A - β - r 41 - 3B2 - 101234 U+0041 - U+03B2 - U+101234
格式化说明符 %c 用于表示字符,当和字符配合使用时, %v 或 %d 会输出用于表示该字符的整数, %U 输出格式为 U+hhhh 的字符串。
Unicode 包中内置了一些用于测试字符的函数,这些函数的返回值都是一个布尔值,如下所示(其中 ch 代表字符):
- 判断是否为字母:unicode.IsLetter(ch)
- 判断是否为数字:unicode.IsDigit(ch)
- 判断是否为空白符号:unicode.IsSpace(ch)
UTF-8和Unicode有何区别?
Unicode 与 ASCII 类似,都是一种字符集。
字符集为每个字符分配一个唯一的 ID,我们使用到的所有字符在 Unicode 字符集中都有一个唯一的 ID,例如上面例子中的 a 在 Unicode 与 ASCII 中的编码都是 97。汉字“你”在 Unicode 中的编码为 20320,在不同国家的字符集中,字符所对应的 ID 也会不同。而无论任何情况下,Unicode 中的字符的 ID 都是不会变化的。
UTF-8 是编码规则,将 Unicode 中字符的 ID 以某种方式进行编码,UTF-8 的是一种变长编码规则,从 1 到 4 个字节不等。编码规则如下:
- 0xxxxxx 表示文字符号 0~127,兼容 ASCII 字符集。
- 从 128 到 0x10ffff 表示其他字符。
根据这个规则,拉丁文语系的字符编码一般情况下每个字符占用一个字节,而中文每个字符占用 3 个字节。
广义的 Unicode 指的是一个标准,它定义了字符集及编码规则,即 Unicode 字符集和 UTF-8、UTF-16 编码等。
数据类型转换
在必要以及可行的情况下,一个类型的值可以被转换成另一种类型的值。由于Go语言不存在隐式类型转换,因此所有的类型转换都必须显式的声明:
// 类型 B 的值 = 类型 B(类型 A 的值) valueOfTypeB = typeB(valueOfTypeA)
类型转换只能在定义正确的情况下转换成功,例如从一个取值范围较小的类型转换到一个取值范围较大的类型(将 int16 转换为 int32)。当从一个取值范围较大的类型转换到取值范围较小的类型时(将 int32 转换为 int16 或将 float32 转换为 int),会发生精度丢失(截断)的情况。
只有相同底层类型的变量之间可以进行相互转换(如将 int16 类型转换成 int32 类型),不同底层类型的变量相互转换时会引发编译错误(如将 bool 类型转换为 int 类型):
package main import ( “fmt” “math” ) func main() { // 输出各数值范围 // 输出几个常见整型类型的数值范围 fmt.Println(“int8 range:”, math.MinInt8, math.MaxInt8) fmt.Println(“int16 range:”, math.MinInt16, math.MaxInt16) fmt.Println(“int32 range:”, math.MinInt32, math.MaxInt32) fmt.Println(“int64 range:”, math.MinInt64, math.MaxInt64) // 初始化一个32位整型值 var a int32 = 1047483647 // 输出变量的十六进制形式和十进制值(使用 fmt.Printf 的%x动词将数值以十六进制格式输出,这一行输出 a 在转换前的 32 位的值) fmt.Printf(“int32: 0x%x %d”, a, a) // 将a变量数值转换为十六进制, 发生数值截断 b := int16(a) // 输出变量的十六进制形式和十进制值 fmt.Printf(“int16: 0x%x %d”, b, b) // 将常量保存为float32类型, math.Pi 是 math 包的常量,默认没有类型,会在引用到的地方自动根据实际类型进行推导,这里 math.Pi 被赋值到变量 c 中,因此类型为 float32 var c float32 = math.Pi // 转换为int类型, 浮点发生精度丢失 fmt.Println(int(c)) }
输出ru
int8 range: -128 127 int16 range: -32768 32767 int32 range: -2147483648 2147483647 int64 range: -9223372036854775808 9223372036854775807 int32: 0x3e6f54ff 1047483647 int16: 0x54ff 21759 3
根据输出结果,16 位有符号整型的范围是 -32768~32767,而变量 a 的值 1047483647 不在这个范围内。1047483647 对应的十六进制为 0x3e6f54ff,转为 int16 类型后,长度缩短一半,也就是在十六进制上砍掉一半,变成 0x54ff,对应的十进制值为 21759。
浮点数在转换为整型时,会将小数部分去掉,只保留整数部分。
type关键字(类型别名)
类型别名是 Go 1.9 版本添加的新功能,主要用于解决代码升级、迁移中存在的类型兼容性问题。在 C/C++ 语言中,代码重构升级可以使用宏快速定义一段新的代码,Go语言中没有选择加入宏,而是解决了重构中最麻烦的类型名变更问题。
区分类型别名与类型定义
定义类型别名的写法为:
type TypeAlias = Type
类型别名规定:TypeAlias 只是 Type 的别名,本质上 TypeAlias 与 Type 是同一个类型,就像一个孩子小时候有小名、乳名,上学后用学名,英语老师又会给他起英文名,但这些名字都指的是他本人。
package main import ( “fmt” ) // 将NewInt定义为int类型 type NewInt int // 将int取一个别名叫IntAlias type IntAlias = int func main() { // 将a声明为NewInt类型 var a NewInt // 查看a的类型名 fmt.Printf(“a type: %T”, a) // 将a2声明为IntAlias类型 var a2 IntAlias // 查看a2的类型名 fmt.Printf(“a2 type: %T”, a2) }
结果显示 a 的类型是 main.NewInt,表示 main 包下定义的 NewInt 类型,a2 类型是 int,IntAlias 类型只会在代码中存在,编译完成时,不会有 IntAlias 类型。
非本地类型不能定义方法
package main import ( “time” ) // 定义time.Duration的别名为MyDuration type MyDuration = time.Duration // 为MyDuration添加一个函数 func (m MyDuration) EasySet(a string) { } func main() { }
代码说明如下:
- 第 8 行,为 time.Duration 设定一个类型别名叫 MyDuration。
- 第 11 行,为这个别名添加一个方法。
编译上面代码报错,信息如下:
cannot define new methods on non-local type time.Duration
编译器提示:不能在一个非本地的类型 time.Duration 上定义新方法,非本地类型指的就是 time.Duration 不是在 main 包中定义的,而是在 time 包中定义的,与 main 包不在同一个包中,因此不能为不在一个包中的类型定义方法。
解决这个问题有下面两种方法:
- 将第 8 行修改为 type MyDuration time.Duration,也就是将 MyDuration 从别名改为类型;
- 将 MyDuration 的别名定义放在 time 包中。
在结构体成员嵌入时使用别名
当类型别名作为结构体嵌入的成员时会发生什么情况呢?请参考下面的代码。
package main import ( “fmt” “reflect” ) // 定义商标结构 type Brand struct { } // 为商标结构添加Show()方法 func (t Brand) Show() { } // 为Brand定义一个别名FakeBrand type FakeBrand = Brand // 定义车辆结构 type Vehicle struct { // 嵌入两个结构 FakeBrand Brand } func main() { // 声明变量a为车辆类型 var a Vehicle // 指定调用FakeBrand的Show a.FakeBrand.Show() // 取a的类型反射对象 ta := reflect.TypeOf(a) // 遍历a的所有成员 for i := 0; i < ta.NumField(); i++ { // a的成员信息 f := ta.Field(i) // 打印成员的字段名和类型 fmt.Printf(“FieldName: %v, FieldType: %v”, f.Name, f.Type. Name()) } }
代码输出如下:
FieldName: FakeBrand, FieldType: Brand FieldName: Brand, FieldType: Brand
代码说明如下:
- 第 9 行,定义商标结构。
- 第 13 行,为商标结构添加 Show() 方法。
- 第 17 行,为 Brand 定义一个别名 FakeBrand。
- 第 20~25 行,定义车辆结构 Vehicle,嵌入 FakeBrand 和 Brand 结构。
- 第 30 行,将 Vechicle 实例化为 a。
- 第 33 行,显式调用 Vehicle 中 FakeBrand 的 Show() 方法。
- 第 36 行,使用反射取变量 a 的反射类型对象,以查看其成员类型。
- 第 39~42 行,遍历 a 的结构体成员。
- 第 45 行,打印 Vehicle 类型所有成员的信息。
这个例子中,FakeBrand 是 Brand 的一个别名,在 Vehicle 中嵌入 FakeBrand 和 Brand 并不意味着嵌入两个 Brand,FakeBrand 的类型会以名字的方式保留在 Vehicle 的成员中。
如果尝试将第 33 行改为:
a.Show()
编译器将发生报错:
ambiguous selector a.Show
在调用 Show() 方法时,因为两个类型都有 Show() 方法,会发生歧义,证明 FakeBrand 的本质确实是 Brand 类型。
运算符的优先级
运算符是用来在程序运行时执行数学或逻辑运算的,在Go语言中,一个表达式可以包含多个运算符,当表达式中存在多个运算符时,就会遇到优先级的问题,此时应该先处理哪个运算符呢?这个就由Go语言运算符的优先级来决定的。
Go语言有几十种运算符,被分成十几个级别,有的运算符优先级不同,有的运算符优先级相同,请看下表。
Go语言运算符优先级和结合性一览表
| 优先级 | 分类 | 运算符 | 结合性 |
|---|---|---|---|
| 1 | 逗号运算符 | , | 从左到右 |
| 2 | 赋值运算符 | =、+=、-=、*=、/=、 %=、 >=、 «=、&=、^=、 | = |
| 3 | 逻辑或 | ||
| 4 | 逻辑与 | && | 从左到右 |
| 5 | 按位或 | ||
| 6 | 按位异或 | ^ | 从左到右 |
| 7 | 按位与 | & | 从左到右 |
| 8 | 相等/不等 | ==、!= | 从左到右 |
| 9 | 关系运算符 | <、<=、>、>= | 从左到右 |
| 10 | 位移运算符 | «、» | 从左到右 |
| 11 | 加法/减法 | +、- | 从左到右 |
| 12 | 乘法/除法/取余 | *(乘号)、/、% | 从左到右 |
| 13 | 单目运算符 | !、*(指针)、& 、++、–、+(正号)、-(负号) | 从右到左 |
| 14 | 后缀运算符 | ( )、[ ]、-> | 从左到右 |
注意:优先级值越大,表示优先级越高。
一下子记住所有运算符的优先级并不容易,还好Go语言中大部分运算符的优先级和数学中是一样的,大家在以后的编程过程中也会逐渐熟悉起来。如果实在搞不清,可以加括号,就像下面这样:
d := a + (b * c)
括号的优先级是最高的,括号中的表达式会优先执行,这样各个运算符的执行顺序就一目了然了。
运算符的结合性是指相同优先级的运算符在同一个表达式中,且没有括号的时候,操作数计算的顺序,通常有从左到右和从右到左两种方式,例如, + 加法运算符的结合性是从左到右,那么表达式 a + b + c 则可以理解为为 (a + b) + c 。