Go开发 之 基础语法(常量、枚举、注释、类型别名、指针)
1、常量(const关键字)
1.1、概念
Go语言中的常量使用关键字 const 定义,用于存储不会改变的数据,常量是在编译时被创建的,即使定义在函数内部也是如此,并且只能是布尔型、数字型(整数型、浮点型和复数)和字符串型。由于编译时的限制,定义常量的表达式必须为能被编译器求值的常量表达式。
常量的定义格式和变量的声明语法类似:const name [type] = value
,例如:
const pi = 3.14159 // 相当于 math.Pi 的近似值
在Go语言中,你可以省略类型说明符 [type],因为编译器可以根据变量的值来推断其类型。
- 显式类型定义: const b string = “abc”
- 隐式类型定义: const b = “abc”
常量的值必须是能够在编译时就能够确定的,可以在其赋值表达式中涉及计算过程,但是所有用于计算的值必须在编译期间就能获得。
- 正确的做法:const c1 = 2/3
- 错误的做法:const c2 = getNumber() // 引发构建错误: getNumber() 用做值
可以批量生成,例如:
const (
a = 1
b
c = 2
d
)
fmt.Println(a, b, c, d) // "1 1 2 2"
如果只是简单地复制右边的常量表达式,其实并没有太实用的价值。但是它可以带来其它的特性,那就是 iota 常量生成器语法。
1.2、iota 常量生成器
常量声明可以使用 iota 常量生成器初始化,它用于生成一组以相似规则初始化的常量,但是不用每行都写一遍初始化表达式。在一个 const 声明语句中,在第一个声明的常量所在的行,iota 将会被置为 0,然后在每一个有常量声明的行加一。示例:
type Weekday int
const (
Sunday Weekday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
)
周日将对应 0,周一为 1,以此类推。
1.3、无类型常量
Go语言的常量有个不同寻常之处。虽然一个常量可以有任意一个确定的基础类型,例如 int 或 float64,或者是类似 time.Duration 这样的基础类型,但是许多常量并没有一个明确的基础类型。
编译器为这些没有明确的基础类型的数字常量提供比基础类型更高精度的算术运算,可以认为至少有 256bit 的运算精度。这里有六种未明确类型的常量类型,分别是无类型的布尔型、无类型的整数、无类型的字符、无类型的浮点数、无类型的复数、无类型的字符串。
通过延迟明确常量的具体类型,不仅可以提供更高的运算精度,而且可以直接用于更多的表达式而不需要显式的类型转换。
math.Pi 无类型的浮点数常量,可以直接用于任意需要浮点数或复数的地方:
var x float32 = math.Pi
var y float64 = math.Pi
var z complex128 = math.Pi
如果 math.Pi 被确定为特定类型,比如 float64,那么结果精度可能会不一样,同时对于需要 float32 或 complex128 类型值的地方则需要一个明确的强制类型转换:
const Pi64 float64 = math.Pi
var x float32 = float32(Pi64)
var y float64 = Pi64
var z complex128 = complex128(Pi64)
对于常量面值,不同的写法可能会对应不同的类型。例如 0、0.0、0i 和 \u0000 虽然有着相同的常量值,但是它们分别对应无类型的整数、无类型的浮点数、无类型的复数和无类型的字符等不同的常量类型。同样,true 和 false 也是无类型的布尔类型,字符串面值常量是无类型的字符串类型。
2、枚举(const和iota枚举)
2.1、概念
Go语言现阶段没有枚举类型,但是可以使用 const 常量的 iota 来模拟枚举类型,如下:
type Weapon int
const (
Arrow Weapon = iota // 开始生成枚举值, 默认为0
Shuriken
SniperRifle
Rifle
Blower
)
// 输出所有枚举值
fmt.Println(Arrow, Shuriken, SniperRifle, Rifle, Blower)
// 使用枚举类型并赋初值
var weapon Weapon = Blower
fmt.Println(weapon)
代码输出如下:
0 1 2 3 4
4
iota高级用法,如:
const (
FlagNone = 1 << iota
FlagRed
FlagGreen
FlagBlue
)
fmt.Printf("%d %d %d\n", FlagRed, FlagGreen, FlagBlue) // 2 4 8
fmt.Printf("%b %b %b\n", FlagRed, FlagGreen, FlagBlue) // 10 100 1000
2.2、将枚举值转换为字符串
直接看代码:
package main
import "fmt"
// 声明芯片类型
type ChipType int
const (
None ChipType = iota
CPU // *处理器
GPU // 图形处理器
)
func (c ChipType) String() string {
switch c {
case None:
return "None"
case CPU:
return "CPU"
case GPU:
return "GPU"
}
return "N/A"
}
func main() {
// 输出CPU的值并以整型格式显示
fmt.Printf("%s %d", CPU, CPU) // CPU 1
}
3、注释(定义及使用)
3.1、定义
Go语言的注释和C/C++的注释一样。主要分成两类,分别是单行注释和多行注释。
- 单行注释简称行注释,是最常见的注释形式,可以在任何地方使用以//开头的单行注释;
- 多行注释简称块注释,以/开头,并以/结尾,且不可以嵌套使用,多行注释一般用于包的文档描述或注释成块的代码片段。
3.2、示例
单行注释的格式如下所示
//单行注释
多行注释的格式如下所示
/*
第一行注释
第二行注释
...
*/
3.3、godoc 工具
godoc 工具会从 Go 程序和包文件中提取*声明的首行注释以及每个对象的相关注释,并生成相关文档,也可以作为一个提供在线文档浏览的 web 服务器,Go语言官网(https://golang.google.cn/)就是通过这种形式实现的。
go get 命令来获取 godoc 工具。如果golang的墙太厚,可以用github:“https://github.com/golang/tools.git”
go get golang.org/x/tools/cmd/godoc
可以直接使用,在命令行输入:godoc -http=:6060
在浏览器输入“http://localhost:6060/pkg/”,可以看到你所以Gopath目录下的src下的项目:
举例看一下某一个项目的doc,如下:
4、类型别名(type关键字)
类型别名是 Go 1.9 版本添加的功能,主要用于解决代码升级、迁移中存在的类型兼容性问题。
在 Go 1.9 版本之前定义内建类型的代码是这样写的:
type byte uint8
type rune int32
而在 Go 1.9 版本之后变为:
type byte = uint8
type rune = int32
这个修改就是配合类型别名而进行的修改。
4.1、区分类型别名与类型定义
定义类型别名的写法为: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\n", a)
// 将a2声明为IntAlias类型
var a2 IntAlias
// 查看a2的类型名
fmt.Printf("a2 type: %T\n", a2)
}
结果:
a type: main.NewInt
a2 type: int
结果显示 a 的类型是 main.NewInt,表示 main 包下定义的 NewInt 类型,a2 类型是 int,IntAlias 类型只会在代码中存在,编译完成时,不会有 IntAlias 类型。
4.2、非本地类型不能定义方法
能够随意地为各种类型起名字,并不意味着可以在自己包里为这些类型任意添加方法,看代码:
package main
import (
"time"
)
// 定义time.Duration的别名为MyDuration
type MyDuration = time.Duration
... ...
这样的代码,编译会报错,
cannot define new methods on non-local type time.Duration
解决方案有两种:
- 1、将第 8 行修改为 type MyDuration time.Duration,也就是将 MyDuration 从别名改为类型;
- 2、将 MyDuration 的别名定义放在 time 包中。
4.3、在结构体成员嵌入时使用别名
当类型别名作为结构体嵌入的成员时,情况如下:
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\n", f.Name, f.Type.
Name())
}
}
结果如下:
FieldName: FakeBrand, FieldType: Brand
FieldName: Brand, FieldType: Brand
5、指针
5.1、概念
5.1.1、Go的指针
Go语言为程序员提供了控制数据结构指针的能力,但并不能进行指针运算。Go语言允许你控制特定集合的数据结构、分配的数量以及内存访问模式,这对于构建运行良好的系统是非常重要的。指针对于性能的影响不言而喻,如果你想要做系统编程、操作系统或者网络应用,指针更是不可或缺的一部分。
指针在Go语言中可以被拆分为两个核心概念:
- 类型指针,允许对这个指针类型的数据进行修改,传递数据可以直接使用指针,无须拷贝数据,类型指针不能进行偏移和运算。
- 切片,由指向起始元素的原始指针、元素数量和容量组成。
受益于这样的约束和拆分,Go语言的指针类型变量即拥有指针高效访问的特点,又不会发生指针偏移,从而避免了非法修改关键性数据的问题。同时,垃圾回收也比较容易对不会发生偏移的指针进行检索和回收。
切片比原始指针具备更强大的特性,而且更为安全。切片在发生越界时,运行时会报出宕机,并打出堆栈,而原始指针只会崩溃。
5.1.2、C/C++中的指针
说到 C/C++ 中的指针,会让许多人谈虎色变,尤其是对指针的偏移、运算和转换。
其实,指针是 C/C++ 语言拥有极高性能的根本所在,在操作大块数据和做偏移时即方便又便捷。因此,操作系统依然使用C语言及指针的特性进行编写。
C/C++ 中指针饱受诟病的根本原因是指针的运算和内存释放,C/C++ 语言中的裸指针可以*偏移,甚至可以在某些情况下偏移进入操作系统的核心区域,我们的计算机操作系统经常需要更新、修复漏洞的本质,就是为解决指针越界访问所导致的“缓冲区溢出”的问题。
要明白指针,需要知道几个概念:指针地址、指针类型和指针取值,下面将展开详细说明。
5.2、认识指针地址和指针类型
一个指针变量可以指向任何一个值的内存地址,它所指向的值的内存地址在 32 和 64 位机器上分别占用 4 或 8 个字节,占用字节的大小与所指向的值的大小无关。当一个指针被定义后没有分配到任何变量时,它的默认值为 nil。指针变量通常缩写为 ptr。
每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go语言中使用在变量名前面添加&操作符(前缀)来获取变量的内存地址(取地址操作),格式:
ptr := &v // v 的类型为 T
其中 v 代表被取地址的变量,变量 v 的地址使用变量 ptr 进行接收,ptr 的类型为*T,称做 T 的指针类型,*代表指针。
举例:
package main
import (
"fmt"
)
func main() {
var cat int = 1
var str string = "banana"
fmt.Printf("%p %p", &cat, &str)
}
结果:
0xc042052088 0xc0420461b0
提示:变量、指针和地址三者的关系是,每个变量都拥有地址,指针的值就是地址。
5.3、从指针获取指针指向的值
当使用&操作符对普通变量进行取地址操作并得到变量的指针后,可以对指针使用*操作符,也就是指针取值,代码如下:
package main
import (
"fmt"
)
func main() {
// 准备一个字符串类型
var house = "Malibu Point 10880, 90265"
// 对字符串取地址, ptr类型为*string
ptr := &house
// 打印ptr的类型
fmt.Printf("ptr type: %T\n", ptr)
// 打印ptr的指针地址
fmt.Printf("address: %p\n", ptr)
// 对指针进行取值操作
value := *ptr
// 取值后的类型
fmt.Printf("value type: %T\n", value)
// 指针取值后就是指向变量的值
fmt.Printf("value: %s\n", value)
}
结果:
ptr type: *string
address: 0xc0420401b0
value type: string
value: Malibu Point 10880, 90265
取地址操作符&和取值操作符*是一对互补操作符,&取出地址,*根据地址取出地址指向的值。
变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:
- 对变量进行取地址操作使用&操作符,可以获得这个变量的指针变量。
- 指针变量的值是指针地址。
- 对指针变量进行取值操作使用*操作符,可以获得指针变量指向的原变量的值。
5.4、使用指针修改值
通过指针不仅可以取值,也可以修改值。
前面已经演示了使用多重赋值的方法进行数值交换,使用指针同样可以进行数值交换,代码如下:
package main
import "fmt"
// 交换函数
func swap(a, b *int) {
// 取a指针的值, 赋给临时变量t
t := *a
// 取b指针的值, 赋给a指针指向的变量
*a = *b
// 将a指针的值赋给b指针指向的变量
*b = t
}
func main() {
// 准备两个变量, 赋值1和2
x, y := 1, 2
// 交换变量值
swap(&x, &y)
// 输出变量值
fmt.Println(x, y)
}
运行结果:
2 1
*操作符作为右值时,意义是取指针的值,作为左值时,也就是放在赋值操作符的左边时,表示 a 指针指向的变量。其实归纳起来,*操作符的根本意义就是操作指针指向的变量。当操作在右值时,就是取指向变量的值,当操作在左值时,就是将值设置给指向的变量。代码如下:
package main
import "fmt"
func swap(a, b *int) {
b, a = a, b
}
func main() {
x, y := 1, 2
swap(&x, &y)
fmt.Println(x, y)
}
结果:
1 2
结果表明,交换是不成功的。上面代码中的 swap() 函数交换的是 a 和 b 的地址,在交换完毕后,a 和 b 的变量值确实被交换。但和 a、b 关联的两个变量并没有实际关联。这就像写有两座房子的卡片放在桌上一字摊开,交换两座房子的卡片后并不会对两座房子有任何影响。
5.5、创建指针的另一种方法——new() 函数
Go语言还提供了另外一种方法来创建指针变量,格式:new(类型)
一般这样写:
str := new(string)
*str = "Go语言教程"
fmt.Println(*str)
new() 函数可以创建一个对应类型的指针,创建过程会分配内存,被创建的指针指向默认值。
5.6、示例:使用指针变量获取命令行的输入信息
Go语言内置的 flag 包实现了对命令行参数的解析,flag 包使得开发命令行工具更为简单。
提前定义一些命令行指令和对应的变量,并在运行时输入对应的参数,经过 flag 包的解析后即可获取命令行的数据。
package main
// 导入系统包
import (
"flag"
"fmt"
)
// 定义命令行参数
var mode = flag.String("mode", "", "process mode")
func main() {
// 解析命令行参数
flag.Parse()
// 输出命令行参数
fmt.Println(*mode)
}
运行:go run main.go --mode=fast
结果:
原理: