Go学习笔记

图片还没有哦

Go语言下载、安装、配置(下载之后其实会自动帮你配好环境变量 如果没有配好的可自行配置)

Go语言标准库常用的包及功能

Go语言标准库包名 功 能
bufio 带缓冲的 I/O 操作
bytes 实现字节操作
container 封装堆、列表和环形列表等容器
crypto 加密算法
database 数据库驱动和接口
debug 各种调试文件格式访问及调试功能
encoding 常见算法如 JSON、XML、Base64 等
flag 命令行解析
fmt 格式化操作
go Go语言的词法、语法树、类型等。可通过这个包进行代码信息提取和修改
html HTML 转义及模板系统
image 常见图形格式的访问及生成
io实现 I/O 原始访问接口及访问封装
math 数学库
net 网络库,支持 Socket、HTTP、邮件、RPC、SMTP 等
os 操作系统平台不依赖平台操作封装
path 兼容各操作系统的路径操作实用函数
plugin Go 1.7 加入的插件系统。支持将代码编译为插件,按需加载
reflect 语言反射支持。可以动态获得代码中的类型信息,获取和修改变量的值
regexp 正则表达式封装
runtime 运行时接口
sort 排序接口
strings 字符串转换、解析及实用函数
time 时间接口
text 文本模板及 Token 词法器

Go语言的目录结构

图片消失啦

一个简单的Go程序

1
2
3
4
5
6
7
package main

import "fmt"

func main() {
fmt.Println("Hello Go")
}

下面是代码说明:
第 1 行,标记当前文件为 main 包,main 包也是 Go 程序的入口包。
第 3 行,导入 fmt 格式化操作包
第 6 行,程序执行的入口函数 main()。
第 6 行,打印Hello Go

Go语言变量声明

了解变量

1
2
3
4
5
6
7
var a int
var b string
var c []float32
var d func() bool
var e struct{
x int
}

代码说明如下:
第1行,声明一个整型类型的变量,可以保存整数数值。
第2行,声明一个字符串类型的变量。
第3行,声明一个 32 位浮点切片类型(动态数组)的变量,浮点切片表示由多个浮点类型组成的数据结构。
第4行,声明一个返回值为布尔类型的函数变量,这种形式一般用于回调函数,即将函数以变量的形式保存下来,在需要的时候重新调用这个函数。
第5行,声明一个结构体类型的变量,这个结构体拥有一个整型的 x 字段。

上面代码的共性是,以 var 关键字开头,要声明的变量名放在中间,而将其类型放在后面。

变量的声明有几种形式,通过下面几节进行整理归纳。

标准变量格式

var 变量名 变量类型

变量声明以关键字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
}
)

使用关键字var和括号,可以将一组变量定义放在一起。

Go语言变量的初始化

变量默认值

回顾C语言声明变量时
在C语言中,变量在声明时,并不会对变量对应内存区域进行清理操作。此时,变量值可能是完全不可预期的结果。开发者需要习惯在使用C语言进行声明时要初始化操作,稍有不慎,就会造成不可预知的后果。

在网络上只有程序员才能看懂的“烫烫烫”和“屯屯屯”的梗,就来源于C/C++中变量默认不初始化。
微软的 VC 编译器会将未初始化的栈空间以 16 进制的 0xCC 填充,而未初始化的堆空间使用 0xCD 填充,而 0xCCCC0xCDCD 在中文的 GB2312 编码中刚好对应“烫”和“屯”字。

因此,如果一个字符串没有结束符\0,直接输出的内存数据转换为字符串就刚好对应“烫烫烫”和“屯屯屯”。

Go在声明变量时
Go会对变量对应的内存区域进行初始化操作。每个变量会初始化其类型的默认值,例如:
整型和浮点型变量的默认值为 0。
字符串变量的默认值为空字符串。
布尔型变量默认为 bool。
切片、函数、指针变量的默认为 nil。

当然,依然可以在变量声明时赋予变量一个初始值。

变量初始化的标准格式

var 变量名 类型 = 表达式

编译器推导类型的格式

在标准格式的基础上,将 int 省略后,编译器会尝试根据等号右边的表达式推导 hp 变量的类型。
var hp = 100
等号右边的部分在编译原理里被称做右值(rvalue)。

下面是编译器根据右值推导变量类型完成初始化的例子。

1
2
3
4
5
var attack = 40
var defence = 20
var damageRate float32 = 0.17
var damage = float32(attack-defence) * damageRate
fmt.Println(damage)

代码说明如下:
第 1 和 2 行,右值为整型 attackdefence 变量的类型为 int
第 3 行,表达式的右值中使用了 0.17Go 语言和C语言一样,编译器会尽量提高精确度,以避免计算中的精度损失。

默认情况下,如果不指定 damageRate 变量的类型,Go语言编译器会将 damageRate 类型推导为 float64 由于这个例子中不需要 float64 的精度,所以强制指定类型为 float32

第 4 行,将 attackdefence 相减后的数值结果依然为整型,使用 float32() 将结果转换为 float32 类型,再与 float32 类型的 damageRate 相乘后 damage 类型也是 float32 类型。

提示:damage 变量的右值是一个复杂的表达式,整个过程既有 attackdefence 的运算还有强制类型转换。强制类型转换会在后面的章节中介绍。

第 5 行,输出damage的值。
以上代码输出结果为:

1
go3.4

短变量声明并初始化

var 的变量声明还有一种更为精简的写法,例如:
hp := 100
这是 Go 语言的推导声明写法,编译器会自动根据右值类型推断出左值的对应类型。

注意:由于使用了:=,而不是赋值的=,因此推导声明写法的左值变量必须是没有定义过的变量。若定义过,将会发生编译错误。

使用了:=就不要使用var不然会报错

1
syntax error: unexpected :=, expecting type

如果 hp 已经被声明过,但依然使用:=时编译器会报错,代码如下:

1
2
3
4
// 声明 hp 变量
var hp int
// 再次声明并赋值
hp := 10

编译报错如下:

1
no new variables on left side of :=

意思是,在:=的左边没有新变量出现,意思就是:=的左边变量已经被声明了。

短变量声明的形式在开发中的例子较多,比如:

1
conn, err := net.Dial("tcp","127.0.0.1:8080")

net.Dial 提供按指定协议和地址发起网络连接,这个函数有两个返回值,一个是连接对象,一个是 err 对象。如果是标准格式将会变成:

1
2
3
var conn net.Conn
var err error
conn, err = net.Dial("tcp", "127.0.0.1:8080")

因此,短变量声明并初始化的格式在开发中使用比较普遍。

注意:在多个短变量声明和赋值中,至少有一个新声明的变量出现在左值中,即便其他变量名可能是重复声明的,编译器也不会报错

代码如下:

1
2
conn, err := net.Dial("tcp", "127.0.0.1:8080")
conn2, err := net.Dial("tcp", "127.0.0.1:8080")

上面的代码片段,编译器不会报err重复定义。

Go语言多个变量同时赋值

两值之间的交换

第一种是用位操作

1
2
3
4
5
6
7
8
9
//可交换两个变量的值
var a int = 100
var b int = 200

a = a ^ b
b = b ^ a
a = a ^ b

fmt.Println(a, b) //结果为 200 100 这里省去了中间值的操作

第二种使用Go的多重赋值特性

1
2
3
4
5
var a int = 100
var b int = 200

b, a = a, b
fmt.Println(a, b) //结果为 200 100

Go语言匿名变量(没有名字的变量)

在使用多重赋值时,如果不需要在左值中接收变量,可以使用匿名变量(anonymous variable)

匿名变量的表现是一个下画线_,使用匿名变量时,只需要在变量声明的地方使用下画线替换即可例如:

1
2
3
4
5
6
func GetData() (int, int) {
return 100, 200
}
a, _ := GetData()
_, b := GetData()
fmt.Println(a, b)

代码运行结果:

1
100 200

Go语言整型(整数类型)

Go 语言中有丰富的数据类型 除了基本的整型浮点型布尔型字符串外,还有切片结构体函数map通道 (channel)等。Go 语言的基本类型和其他语言大同小异切片类型有着指针的便利性,但比指针更为安全,很多高级语言都配有切片进行安全和高效率的内存操作。

本节我们将介绍最基本的整型。

整型分为以下两个大类

按长度分为:int8、int16、int32、int64
还有对应的无符号整型:uint8、uint16、uint32、uint64

其中,uint8 就是我们熟知的 byte 型,int16 对应C语言中的 short 型,int64 对应C语言中的 long 型。

自动匹配平台的 int 和 uint

Go 语言也有自动匹配特定平台整型长度的类型—— intuint

可以跨平台的编程语言可以运行在多种平台上。平台的字节长度是有差异的。64位平台现今已经较为普及,但 8 位、16位、32 位的操作系统依旧存在。16位 平台上依然可以使用 64位 的变量,但运行性能和内存性能上较差。同理,在 64 位平台上大量使用 8位、16位 等与平台位数不等长的变量时,编译器也是尽量将内存对齐以获得最好的性能。

不能正确匹配平台字节长度的程序就类似于用轿车运一头牛和用一辆卡车运送一头牛的情形一样。

在使用 int 和 uint类型 时,不能假定它是 32位64位 的整型,而是考虑 int 和 uint 可能在不同平台上的差异。

哪些情况下使用 int 和 uint

逻辑对整型范围没有特殊需求。例如,对象的长度使用内建 len() 函数返回,这个长度可以根据不同平台的字节长度进行变化。实际使用中,切片或 map 的元素数量等都可以用 int 来表示

反之,在二进制传输、读写文件的结构描述时,为了保持文件的结构不会受到不同编译目标平台字节长度的影响,不要使用 int 和 uint

Go语言浮点类型(小数类型)

Go语言支持两种浮点型数:float32float64。这两种浮点型数据格式遵循 IEEE 754 标准
float32 的浮点数的最大范围约为 3.4e38,可以使用常量定义:math.MaxFloat32
float64 的浮点数的最大范围约为 1.8e308,可以使用一个常量定义:math.MaxFloat64

打印浮点数时,可以使用 fmt 包配合动词 %f,代码如下:

1
2
3
4
5
6
7
8
9
package main
import (
"fmt"
"math"
)
func main() {
fmt.Printf("%f\n", math.Pi)
fmt.Printf("%.2f\n", math.Pi)
}

代码说明如下:
第 7 行,按默认宽度和精度输出整型。
第 8 行,按默认宽度,2 位精度输出(小数点后的位数)。

代码运行结果如下:

1
2
3.141593
3.14

Go语言bool类型(布尔类型)

布尔型数据在 Go 语言中以 bool 类型进行声明,布尔型数据只有 true(真)和 false(假)两个值。

Go 语言中不允许将整型强制转换为布尔型,代码如下:

1
2
var n bool
fmt.Println(int(n) * 2)

编译错误,输出如下:

1
cannot convert n (type bool) to type int

布尔型无法参与数值运算,也无法与其他类型进行转换。

Go语言字符串

字符串在 Go 语言中以原生数据类型出现,使用字符串就像使用其他原生数据类型(int、bool、float32、float64 等)一样。

提示:在 C++、C# 语言中,字符串以类的方式进行封装。

C# 语言中在使用泛型匹配约束类型时,字符串是以 Class 的方式存在,而不是 String,因为并没有“字符串”这种原生数据类型。

C++ 语言中使用模板匹配类型时,为了使字符串与其他原生数据类型一样支持赋值操作,需要对字符串类进行操作符重载。

字符串的值为双引号中的内容,可以在 Go 语言的源码中直接添加非 ASCII 码字符,代码如下:

1
2
str := "hello world"
ch := "中文"

字符串转义符

Go 语言的字符串常见转义符包含回车、换行、单双引号、制表符等,如下表所示。

Go语言的常见转义符

转移符 含义
\r 回车符(返回行首)
\n 换行符(直接跳到下一行的同列位置)
\t 制表符
\' 单引号
\" 双引号
\\ 反斜杠

在 Go 语言源码中使用转义符代码如下:

1
2
3
4
5
6
7
package main
import (
"fmt"
)
func main() {
fmt.Println("str := \"c:\\Go\\bin\\go.exe\"")
}

代码运行结果:

1
str := "c:\Go\bin\go.exe"

这段代码中将双引号和反斜杠"\"进行转义。

字符串实现基于 UTF-8 编码

Go 语言里的字符串的内部实现使用 UTF-8 编码。通过 rune 类型,可以方便地对每个 UTF-8 字符进行访问。当然,Go 语言也支持按传统的 ASCII 码方式进行逐字符访问。

提示:Python 语言的 2.0 版本不是基于 UTF-8 编码设计,到了 3.0 版才改为 UTF-8 编码设计。因此,使用 2.0 版本时,在编码上会出现很多混乱情况。

同样,C/C++语言的 std::string 在使用 UTF-8 时,经常因为没有方便的 UTF-8 配套封装让编写极为困难。

关于字符串的 UTF-8 字符访问的详细方法,后面内容将会详细介绍。

定义多行字符串

在源码中,将字符串的值以双引号书写的方式是字符串的常见表达方式,被称为字符串字面量(string literal)。这种双引号字面量不能跨行。如果需要在源码中嵌入一个多行字符串时,就必须使用`字符,代码如下:

1
2
3
4
5
6
const str = ` 第一行
第二行
第三行
\r\n
`
fmt.Println(str)

代码运行结果:

1
2
3
4
第一行
第二行
第三行
\r\n

`叫反引号,就是键盘上 1 键左边的键,两个反引号间的字符串将被原样赋值到 str 变量中。

在这种方式下,反引号间换行将被作为字符串中的换行,但是所有的转义字符均无效,文本将会原样输出。

多行字符串一般用于内嵌源码和内嵌数据等,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const codeTemplate = `// Generated by github.com/davyxu/cellnet/
protoc-gen-msg
// DO NOT EDIT!{{range .Protos}}
// Source: {{.Name}}{{end}}
package {{.PackageName}}
{{if gt .TotalMessages 0}}
import (
"github.com/davyxu/cellnet"
"reflect"
_ "github.com/davyxu/cellnet/codec/pb"
)
{{end}}
func init() {
{{range .Protos}}
// {{.Name}}{{range .Messages}}
cellnet.RegisterMessageMeta("pb","{{.FullName}}", reflect.TypeOf((*{{.Name}})(nil)).Elem(), {{.MsgID}}) {{end}}
{{end}}
}
`

这段代码只定义了一个常量 codeTemplate,类型为字符串,使用`定义。字符串的内容为一段代码生成中使用到的 Go 源码格式。

在`间的所有代码均不会被编译器识别,而只是作为字符串的一部分。

字符串的常见用法

Go语言计算字符串长度——len()和RuneCountInString()

Go 语言的内建函数 len(),可以用来获取切片、字符串、通道(channel)等的长度。下面的代码可以用 len() 来获取字符串的长度。

代码如下:

1
2
3
4
tip1 := "genji is a ninja"
fmt.Println(len(tip1))
tip2 := "忍者"
fmt.Println(len(tip2))

程序输出如下:

1
2
16
6

len() 函数的返回值的类型为 int,表示字符串的 ASCII 字符个数或字节长度。
输出中第一行的 16 表示 tip1 的字符个数为 16。
输出中第二行的 6 表示 tip2 的字符格式,也就是“忍者”的字符个数是 6,然而根据习惯,“忍者”的字符个数应该是 2。

这里的差异是由于 Go 语言的字符串都以 UTF-8 格式保存,每个中文占用 3 个字节,因此使用 len() 获得两个中文文字对应的 6 个字节。

如果希望按习惯上的字符个数来计算,就需要使用 Go 语言中 UTF-8 包提供的 RuneCountInString() 函数,统计 Uncode 字符数量。

下面的代码展示如何计算UTF-8的字符个数。
代码如下:

1
2
fmt.Println(utf8.RuneCountInString("忍者"))
fmt.Println(utf8.RuneCountInString("龙忍出鞘,fight!"))

程序输出如下:

1
2
2
11

一般游戏中在登录时都需要输入名字,而名字一般有长度限制。考虑到国人习惯使用中文做名字,就需要检测字符串 UTF-8 格式的长度。

总结

ASCII 字符串长度使用 len() 函数。
Unicode 字符串长度使用 utf8.RuneCountInString() 函数。

Go语言遍历字符串——获取每一个字符串元素

遍历字符串有下面两种写法。
遍历每一个ASCII字符
遍历 ASCII 字符使用 for 的数值循环进行遍历,直接取每个字符串的下标获取 ASCII 字符,如下面的例子所示。

1
2
3
4
theme := "狙击 start"
for i := 0; i < len(theme); i++ {
fmt.Printf("ascii: %c %d\n", theme[i], theme[i])
}

程序输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
ascii: ?  231
ascii: 139
ascii: 153
ascii: ? 229
ascii: 135
ascii: ? 187
ascii: 32
ascii: s 115
ascii: t 116
ascii: a 97
ascii: r 114
ascii: t 116

这种模式下取到的汉字“惨不忍睹”。由于没有使用 Unicode,汉字被显示为乱码。
Unicode 字符遍历字符串
同样的内容:

1
2
3
4
theme := "狙击 start"
for _, s := range theme {
fmt.Printf("Unicode: %c %d\n", s, s)
}

程序输出如下:

1
2
3
4
5
6
7
8
Unicode: 狙  29401
Unicode: 击 20987
Unicode: 32
Unicode: s 115
Unicode: t 116
Unicode: a 97
Unicode: r 114
Unicode: t 116

可以看到,这次汉字可以正常输出了。

总结

ASCII 字符串遍历直接使用下标。
Unicode 字符串遍历用 for range

Go语言字符串截取(获取字符串的某一段字符)

获取字符串的某一段字符是开发中常见的操作,我们一般将字符串中的某一段字符称做子串(substring)。

下面例子中使用 strings.Index() 函数在字符串中搜索另外一个子串,代码如下:

1
2
3
4
tracer := "死神来了, 死神bye bye"
comma := strings.Index(tracer, ", ")
pos := strings.Index(tracer[comma:], "死神")
fmt.Println(comma, pos, tracer[comma+pos:])

程序输出如下:

1
12 3 死神bye bye

代码说明如下:

1) 第 2 行尝试在 tracer 的字符串中搜索中文的逗号,返回的位置存在 comma 变量中,类型是 int,表示从 tracer 字符串开始的 ASCII 码位置。

strings.Index() 函数并没有像其他语言一样,提供一个从某偏移开始搜索的功能。不过我们可以对字符串进行切片操作来实现这个逻辑。

2) 第4行中,tracer[comma:] 从 tracer 的 comma 位置开始到 tracer 字符串的结尾构造一个子字符串,返回给 string.Index() 进行再索引。得到的 pos 是相对于 tracer[comma:] 的结果。

comma 逗号的位置是 12,而 pos 是相对位置,值为 3。我们为了获得第二个“死神”的位置,也就是逗号后面的字符串,就必须让 comma 加上 pos 的相对偏移,计算出 15 的偏移,然后再通过切片 tracer[comma+pos:] 计算出最终的子串,获得最终的结果:“死神bye bye”。

总结

字符串索引比较常用的有如下几种方法:
strings.Index:正向搜索子字符串。
strings.LastIndex:反向搜索子字符串。
搜索的起始位置可以通过切片偏移制作。

Go语言修改字符串

Go 语言的字符串无法直接修改每一个字符元素,只能通过重新构造新的字符串并赋值给原来的字符串变量实现。请参考下面的代码:

1
2
3
4
5
6
angel := "Heros never die"
angleBytes := []byte(angel)
for i := 5; i <= 10; i++ {
angleBytes[i] = ' '
}
fmt.Println(string(angleBytes))

程序输出如下:

1
Heros       die

代码说明如下:
在第 2 行中,将字符串转为字符串数组。
第 3~5 行利用循环,将 never 单词替换为空格。
最后打印结果。

感觉我们通过代码达成了修改字符串的过程,但真实的情况是:Go 语言中的字符串和其他高级语言(Java、C#)一样,默认是不可变的(immutable)。

字符串不可变有很多好处,如天生线程安全,大家使用的都是只读对象,无须加锁;再者,方便内存共享,而不必使用写时复制(Copy On Write)等技术;字符串 hash 值也只需要制作一份。

所以说,代码中实际修改的是 []byte , []byteGo 语言中是可变的,本身就是一个切片。

在完成了对 []byte 操作后,在第 6 行,使用 string()[]byte 转为字符串时,重新创造了一个新的字符串。
总结
Go 语言的字符串是不可变的。
修改字符串时,可以将字符串转换为 []byte 进行修改。
[]bytestring 可以通过强制类型转换互转。

Go语言字符串拼接(连接)

连接字符串这么简单,还需要学吗?确实,Go 语言和大多数其他语言一样,使用+对字符串进行连接操作,非常直观。

但问题来了,好的事物并非完美,简单的东西未必高效。除了加号连接字符串,Go 语言中也有类似于 StringBuilder 的机制来进行高效的字符串连接,例如:

1
2
3
4
5
6
7
8
9
hammer := "吃我一锤"
sickle := "死吧"
// 声明字节缓冲
var stringBuilder bytes.Buffer
// 把字符串写入缓冲
stringBuilder.WriteString(hammer)
stringBuilder.WriteString(sickle)
// 将缓冲以字符串形式输出
fmt.Println(stringBuilder.String())

bytes.Buffer 是可以缓冲并可以往里面写入各种字节数组的。字符串也是一种字节数组,使用 WriteString() 方法进行写入。

将需要连接的字符串,通过调用 WriteString() 方法,写入 stringBuilder 中,然后再通过 stringBuilder.String() 方法将缓冲转换为字符串。

Go语言fmt.Sprintf(格式化输出)

格式化在逻辑中非常常用。使用格式化函数,要注意写法:
fmt.Sprintf(格式化样式, 参数列表…)

格式化样式:字符串形式,格式化动词以%开头。
参数列表:多个参数以逗号分隔,个数必须与格式化样式中的个数一一对应,否则运行时会报错。

在 Go 语言中,格式化的命名延续C语言风格:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var progress = 2
var target = 8
// 两参数格式化
title := fmt.Sprintf("已采集%d个药草, 还需要%d个完成任务", progress, target)
fmt.Println(title)
pi := 3.14159
// 按数值本身的格式输出
variant := fmt.Sprintf("%v %v %v", "月球基地", pi, true)
fmt.Println(variant)
// 匿名结构体声明, 并赋予初值
profile := &struct {
Name string
HP int
}{
Name: "rat",
HP: 150,
}
fmt.Printf("使用'%%+v' %+v\n", profile)
fmt.Printf("使用'%%#v' %#v\n", profile)
fmt.Printf("使用'%%T' %T\n", profile)

代码输出如下:

1
2
3
4
5
已采集2个药草, 还需要8个完成任务
"月球基地" 3.14159 true
使用'%+v' &{Name:rat HP:150}
使用'%#v' &struct { Name string; HP int }{Name:"rat", HP:150}
使用'%T' *struct { Name string; HP int }

C语言中, 使用%d代表整型参数
下表中标出了常用的一些格式化样式中的动词及功能。

表:字符串格式化时常用动词及功能
| 动 词 | 功 能 |
| :—: | :—-: ||
| %v | 按值的本来值输出 |
| %+v | 在 %v 基础上,对结构体字段名和值进行展开 |
| %#v | 输出 Go 语言语法格式的值 |
| %T | 输出 Go 语言语法格式的类型和值 |
| %% | 输出 % 本体 |
| %b | 整型以二进制方式显示 |
| %o | 整型以八进制方式显示 |
| %d | 整型以十进制方式显示 |
| %x | 整型以十六进制方式显示 |
| %X | 整型以十六进制、字母大写方式显示 |
| %U | Unicode 字符 |
| %f | 浮点数 |
| %p | 指针,十六进制方式显示 |

fmt.Println 与 fmt.Printf 的区别,以及 fmt.Printf 的详细用法

Go语言Base64编码——电子邮件的基础编码格式

Base64 编码是常见的对 8 比特字节码的编码方式之一。Base64 可以使用 64 个可打印字符来表示二进制数据,电子邮件就是使用这种编码。

Go 语言的标准库自带了 Base64 编码算法,通过几行代码就可以对数据进行编码,示例代码如下。

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 (
"encoding/base64"
"fmt"
)
func main() {
// 需要处理的字符串
message := "Away from keyboard. https://golang.org/"
// 编码消息
encodedMessage := base64.StdEncoding.EncodeToString([]byte (message))
// 输出编码完成的消息
fmt.Println(encodedMessage)
// 解码消息
data, err := base64.StdEncoding.DecodeString(encodedMessage)
// 出错处理
if err != nil {
fmt.Println(err)
} else {
// 打印解码完成的数据
fmt.Println(string(data))
}
}

代码说明如下:
第 8 行为需要编码的消息,消息可以是字符串,也可以是二进制数据。
第 10 行,base64 包有多种编码方法,这里使用 base64.StdEnoding 的标准编码方法进行编码。传入的字符串需要转换为字节数组才能供这个函数使用。
第 12 行,编码完成后一定会输出字符串类型,打印输出。
第 14 行,解码时可能会发生错误,使用 err 变量接收错误。
第 17 行,出错时,打印错误。
第 20 行,正确时,将返回的字节数组([]byte)转换为字符串。

Go语言字符类型(byte和rune)

字符串中的每一个元素叫做“字符”,在遍历或者单个获取字符串元素时可以获得字符。

Go 语言的字符有以下两种:
一种是 uint8 类型,或者叫 byte 型,代表了 ASCII 码的一个字符。
另一种是 rune 类型,代表一个 UTF-8 字符。当需要处理中文、日文或者其他复合字符时,则需要用到 rune 类型。rune 类型实际是一个 int32

使用 fmt.Printf 中的%T动词可以输出变量的实际类型,使用这个方法可以查看 byterune 的本来类型,代码如下:

1
2
3
4
var a byte = 'a'
fmt.Printf("%d %T\n", a, a)
var b rune = '你'
fmt.Printf("%d %T\n", b, b)

例子输出结果:

1
2
97 uint8
20320 int32

可以发现,byte 类型的 a 变量,实际类型是 uint8,其值为 ‘a’,对应的 ASCII 编码为 97。

rune 类型的 b 变量的实际类型是 int32,对应的 Unicode 码就是 20320

Go 使用了特殊的 rune 类型来处理 Unicode,让基于 Unicode 的文本处理更为方便,也可以使用 byte 型进行默认字符串处理,性能和扩展性都有照顾。

UTF-8 和 Unicode 有何区别?

Unicode 是字符集,ASCII 也是一种字符集。

字符集为每个字符分配一个唯一的 ID,我们使用到的所有字符在 Unicode 字符集中都有唯一的一个 ID 对应,例如上面例子中的 a 在 UnicodeASCII 中的编码都是 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语言数据类型转换

Go 语言使用类型前置加括号的方式进行类型转换,一般格式如下:

1
T(表达式)

其中,T 代表要转换的类型。表达式包括变量、复杂算子和函数返回值等。

类型转换时,需要考虑两种类型的关系和范围,是否会发生数值截断等,参见下面代码:

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"
"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("int32: 0x%x %d\n", a, a)
// 将a变量数值转换为十六进制, 发生数值截断
b := int16(a)
// 输出变量的十六进制形式和十进制值
fmt.Printf("int16: 0x%x %d\n", b, b)
// 将常量保存为float32类型
var c float32 = math.Pi
// 转换为int类型, 浮点发生精度丢失
fmt.Println(int(c))
}

代码说明如下:
第 11~14 行,输出常见整型类型的数值范围。
第 17 行,声明 int32 类型的 a 变量并初始化。
第 19 行,使用 fmt.Printf 的%x动词将数值以十六进制格式输出。这一行输出 a 在转换前的 32 位的值。
第 22 行,将 a 的值转换为 int16 类型,也就是从 32 位有符号整型转换为 16 位有符号整型。由于 16 位变量没有 32 位变量的数值范围大,因此数值会进行截断。
第 24 行,输出转换后的 a 变量值,也就是 b 的值。同样以十六进制和十进制两种方式进行打印。
第 27 行,math.Pi 是 math 包的常量,默认没有类型,会在引用到的地方自动根据实际类型进行推导。这里 math.Pi 被存到 c 中,类型为 float32。
第 29 行,将 float32 转换为 int 类型并输出。

代码输出如下:

1
2
3
4
5
6
7
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,转为 16 位变量后,长度缩短一半,也就是在十六进制上砍掉一半,变成 0x54ff,对应的十进制值为 21759。

浮点数在转换为整型时,会将小数部分去掉,只保留整数部分。

整型截断在类型转换中发生的较为隐性,有些即为难追查的问题,很小一部分是由整型截断造成。

Go语言指针详解,看这一篇文章就够了

指针(pointer)概念在 Go 语言中被拆分为两个核心概念:
类型指针,允许对这个指针类型的数据进行修改。传递数据使用指针,而无须拷贝数据。类型指针不能进行偏移和运算。
切片,由指向起始元素的原始指针、元素数量和容量组成。

受益于这样的约束和拆分,Go 语言的指针类型变量拥有指针的高效访问,但又不会发生指针偏移,从而避免非法修改关键性数据问题。同时,垃圾回收也比较容易对不会发生偏移的指针进行检索和回收。

切片比原始指针具备更强大的特性,更为安全。切片发生越界时,运行时会报出宕机,并打出堆栈,而原始指针只会崩溃。
C/C++中的指针
说到 C/C++ 中的指针,会让许多人“谈虎色变”,尤其对指针偏移、运算、转换都非常恐惧。

其实,指针是使 C/C++ 语言有极高性能的根本,在操作大块数据和做偏移时方便又便捷。因此,操作系统依然使用C语言及指针特性进行编写。

C/C++ 中指针饱受诟病的根本原因是指针运算和内存释放。

C/C++ 语言中的裸指针可以自由偏移,甚至可以在某些情况下偏移进入操作系统核心区域。我们的计算机操作系统经常需要更新、修复漏洞的本质,是为解决指针越界访问所导致的“缓冲区溢出”。

要明白指针,需要知道几个概念:指针地址、指针类型和指针取值,下面将展开细说。

认识指针地址和指针类型

每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go 语言中使用&作符放在变量前面对变量进行“取地址”操作。

格式如下:

1
ptr := &v    // v的类型为T

其中 v 代表被取地址的变量,被取地址的 v 使用 ptr 变量进行接收,ptr 的类型就为 T,称做 T 的指针类型。 代表指针。

指针实际用法,通过下面的例子了解:

1
2
3
4
5
6
7
8
9
package main
import (
"fmt"
)
func main() {
var cat int = 1
var str string = "banana"
fmt.Printf("%p %p", &cat, &str)
}

运行结果:

1
0xc042052088 0xc0420461b0

代码说明如下:
第 6 行,声明整型 cat 变量。
第 7 行,声明字符串 str 变量。
第 8 行,使用 fmt.Printf 的动词%p输出 cat 和 str 变量取地址后的指针值,指针值带有0x的十六进制前缀。

输出值在每次运行是不同的,代表 cat 和 str 两个变量在运行时的地址。

在 32 位平台上,将是 32 位地址;64 位平台上是 64 位地址。

提示:变量、指针和地址三者的关系是:每个变量都拥有地址,指针的值就是地址。

从指针获取指针指向的值

在对普通变量使用&操作符取地址获得这个变量的指针后,可以对指针使用*操作,也就是指针取值,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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)
}

运行结果:

1
2
3
4
ptr type: *string
address: 0xc0420401b0
value type: string
value: Malibu Point 10880, 90265

代码说明如下:
第 7 行,准备一个字符串并赋值。
第 9 行,对字符串取地址,将指针保存到 ptr 中。
第 11 行,打印 ptr 变量的类型,类型为 *string。
第 13 行,打印 ptr 的指针地址,每次运行都会发生变化。
第 15 行,对 ptr 指针变量进行取值操作,value 变量类型为 string。
第 17 行,打印取值后 value 的类型。
第 19 行,打印 value 的值。

取地址操作符&和取值操作符 是一对互补操作符,&取出地址, 根据地址取出地址指向的值。

变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:
对变量进行取地址(&)操作,可以获得这个变量的指针变量。
指针变量的值是指针地址。
对指针变量进行取值(*)操作,可以获得指针变量指向的原变量的值。
使用指针修改值
通过指针不仅可以取值,也可以修改值。

前面已经使用多重赋值的方法进行数值交换,使用指针同样可以进行数值交换,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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)
}

运行结果:

1
2 1

代码说明如下:
第 4 行,定义一个交换函数,参数为 a、b,类型都为 int,都是指针类型。
第 6 行,将 a 指针取值,把值(int类型)赋给 t 变量,t 此时也是 int 类型。
第 8 行,取 b 指针值,赋给 a 变量指向的变量。注意,此时
a的意思不是取 a 指针的值,而是“a指向的变量”。
第 10 行,将 t 的值赋给 b 指向的变量。
第 14 行,准备 x、y 两个变量,赋值 1 和 2,类型为 int。
第 16 行,取出 x 和 y 的地址作为参数传给 swap() 函数进行调用。
第 18 行,交换完毕时,输出 x 和 y 的值。

操作符作为右值时,意义是取指针的值;作为左值时,也就是放在赋值操作符的左边时,表示 a 指向的变量。其实归纳起来, 操作符的根本意义就是操作指针指向的变量。当操作在右值时,就是取指向变量的值;当操作在左值时,就是将值设置给指向的变量。

如果在 swap() 函数中交换操作的是指针值,会发生什么情况?可以参考下面代码:

1
2
3
4
5
6
7
8
9
10
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
1 2

结果表明,交换是不成功的。上面代码中的 swap() 函数交换的是 a 和 b 的地址,在交换完毕后,a 和 b 的变量值确实被交换。但和 a、b 关联的两个变量并没有实际关联。这就像写有两座房子的卡片放在桌上一字摊开,交换两座房子的卡片后并不会对两座房子有任何影响。

示例:使用指针变量获取命令行的输入信息

Go 语言的 flag 包中,定义的指令以指针类型返回。通过学习 flag 包,可以深入了解指针变量在设计上的方便之处。

下面的代码通过提前定义一些命令行指令和对应变量,在运行时,输入对应参数的命令行参数后,经过 flag 包的解析后即可通过定义的变量获取命令行的数据。

获取命令行输入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main
// 导入系统包
import (
"flag"
"fmt"
)
// 定义命令行参数
var mode = flag.String("mode", "", "process mode")
func main() {
// 解析命令行参数
flag.Parse()
// 输出命令行参数
fmt.Println(*mode)
}

将这段代码命名为main.go,然后使用如下命令行运行:

1
$ go run flagparse.go --mode=fast

命令行输出结果如下:

1
fast

代码说明如下:
第 8 行,通过 flag.String,定义一个 mode 变量,这个变量的类型是 * string。后面 3 个参数分别如下:
参数名称:在给应用输入参数时,使用这个名称。
参数值的默认值:与 flag 所使用的函数创建变量类型对应,String 对应字符串、Int 对应整型、Bool 对应布尔型等。
参数说明:使用 -help 时,会出现在说明中。
第 11 行,解析命令行参数,并将结果写入创建的指令变量中,这个例子中就是 mode 变量。
第 13 行,打印 mode 指针所指向的变量。

由于之前使用 flag.String 已经注册了一个 mode 的命令行参数,flag 底层知道怎么解析命令行,并且将值赋给 mode*string 指针。在 Parse 调用完毕后,无须从 flag 获取值,而是通过自己注册的 mode 这个指针,获取到最终的值。代码运行流程如下图所示。

图:命令行参数与变量的关系
还没有图片哦

创建指针的另一种方法——new() 函数

Go 语言还提供了另外一种方法来创建指针变量,格式如下:

1
new(类型)

一般这样写:

1
2
3
str := new(string)
*str = "ninja"
fmt.Println(*str)

new() 函数可以创建一个对应类型的指针,创建过程会分配内存。被创建的指针指向的值为默认值。

Go语言变量生命期,Go语言变量逃逸分析

讨论变量生命期之前,先来了解下计算机组成里两个非常重要的概念:堆和栈。

什么是栈

栈(Stack)是一种拥有特殊规则的线性表数据结构。
栈只允许往线性表的一端放入数据,之后在这一端取出数据,按照后进先出(LIFO,Last InFirst Out)的顺序,如下图所示。

还没有图片哦

往栈中放入元素的过程叫做入栈。入栈会增加栈的元素数量,最后放入的元素总是位于栈的顶部,最先放入的元素总是位于栈的底部。

从栈中取出元素时,只能从栈顶部取出。取出元素后,栈的数量会变少。最先放入的元素总是最后被取出,最后放入的元素总是最先被取出。不允许从栈底获取数据,也不允许对栈成员(除栈顶外的成员)进行任何查看和修改操作。

栈的原理类似于将书籍一本一本地堆起来。书按顺序一本一本从顶部放入,要取书时只能从顶部一本一本取出。

变量和栈有什么关系

栈可用于内存分配,栈的分配和回收速度非常快。下面代码展示栈在内存分配上的作用,代码如下:

1
2
3
4
5
6
7
func calc(a, b int) int {
var c int
c = a * b
var x int
x = c * 10
return x
}

代码说明如下:
第 1 行,传入 a、b 两个整型参数。
第 2 行,声明 c 整型变量,运行时,c 会分配一段内存用以存储 c 的数值。
第 3 行,将 a 和 b 相乘后赋予 c。
第 4 行,声明 x 整型变量,x 也会被分配一段内存。
第 5 行,让 c 乘以 10 后存储到 x 变量中。
第 6 行,返回 x 的值。

上面的代码在没有任何优化情况下,会进行 c 和 x 变量的分配过程。Go 语言默认情况下会将 c 和 x 分配在栈上,这两个变量在 calc() 函数退出时就不再使用,函数结束时,保存 c 和 x 的栈内存再出栈释放内存,整个分配内存的过程通过栈的分配和回收都会非常迅速。

什么是堆

堆在内存分配中类似于往一个房间里摆放各种家具,家具的尺寸有大有小。分配内存时,需要找一块足够装下家具的空间再摆放家具。经过反复摆放和腾空家具后,房间里的空间会变得乱七八糟,此时再往空间里摆放家具会存在虽然有足够的空间,但各空间分布在不同的区域,无法有一段连续的空间来摆放家具的问题。此时,内存分配器就需要对这些空间进行调整优化,如下图所示。

还没有图片哦

堆分配内存和栈分配内存相比,堆适合不可预知大小的内存分配。但是为此付出的代价是分配速度较慢,而且会形成内存碎片。
变量逃逸(Escape Analysis)——自动决定变量分配方式,提高运行效率
堆和栈各有优缺点,该怎么在编程中处理这个问题呢?在 C/C++ 语言中,需要开发者自己学习如何进行内存分配,选用怎样的内存分配方式来适应不同的算法需求。比如,函数局部变量尽量使用栈;全局变量、结构体成员使用堆分配等。程序员不得不花费很多年的时间在不同的项目中学习、记忆这些概念并加以实践和使用。

Go 语言将这个过程整合到编译器中,命名为“变量逃逸分析”。这个技术由编译器分析代码的特征和代码生命期,决定应该如何堆还是栈进行内存分配,即使程序员使用 Go 语言完成了整个工程后也不会感受到这个过程。

逃逸分析

使用下面的代码来展现 Go 语言如何通过命令行分析变量逃逸,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main
import "fmt"
// 本函数测试入口参数和返回值情况
func dummy(b int) int {
// 声明一个c赋值进入参数并返回
var c int
c = b
return c
}
// 空函数, 什么也不做
func void() {
}
func main() {
// 声明a变量并打印
var a int
// 调用void()函数
void()
// 打印a变量的值和dummy()函数返回
fmt.Println(a, dummy(0))
}

代码说明如下:
第 4 行,dummy() 函数拥有一个参数,返回一个整型值,测试函数参数和返回值分析情况。
第 6 行,声明 c 变量,这里演示函数临时变量通过函数返回值返回后的情况。
第 11 行,这是一个空函数,测试没有任何参数函数的分析情况。
第 15 行,在 main() 中声明 a 变量,测试 main() 中变量的分析情况。
第 17 行,调用 void() 函数,没有返回值,测试 void() 调用后的分析情况。
第 19 行,打印 a 和 dummy(0) 的返回值,测试函数返回值没有变量接收时的分析情况。

接着使用如下命令行运行上面的代码:

1
$ go run -gcflags "-m -l" main.go

使用 go run 运行程序时,-gcflags 参数是编译参数。其中 -m 表示进行内存分配分析,-l 表示避免程序内联,也就是避免进行程序优化。

运行结果如下:

1
2
3
4
5
6
# command-line-arguments
./main.go:29:13: a escapes to heap
./main.go:29:22: dummy(0) escapes to heap
./main.go:29:13: main ... argument does not escape

0 0

程序运行结果分析如下:
输出第 2 行告知“main 的第 29 行的变量 a 逃逸到堆”。
第 3 行告知“dummy(0)调用逃逸到堆”。由于 dummy() 函数会返回一个整型值,这个值被 fmt.Println 使用后还是会在其声明后继续在 main() 函数中存在。
第 4 行,这句提示是默认的,可以忽略。

上面例子中变量 c 是整型,其值通过 dummy() 的返回值“逃出”了 dummy() 函数。c 变量值被复制并作为 dummy() 函数返回值返回,即使 c 变量在 dummy() 函数中分配的内存被释放,也不会影响 main() 中使用 dummy() 返回的值。c 变量使用栈分配不会影响结果。

取地址发生逃逸

下面的例子使用结构体做数据,了解在堆上分配的情况,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main
import "fmt"
// 声明空结构体测试结构体逃逸情况
type Data struct {
}
func dummy() *Data {
// 实例化c为Data类型
var c Data
//返回函数局部变量地址
return &c
}
func main() {
fmt.Println(dummy())
}

代码说明如下:

第 4 行,声明一个空的结构体做结构体逃逸分析。
第 6 行,将 dummy() 函数的返回值修改为 *Data 指针类型。
第 8 行,将 c 变量声明为 Data 类型,此时 c 的结构体为值类型。
第 10 行,取函数局部变量 c 的地址并返回。Go 语言的特性允许这样做。
第 13 行,打印 dummy() 函数的返回值。

执行逃逸分析:

1
2
3
4
5
6
7
$ go run -gcflags "-m -l" main.go
# command-line-arguments
./main.go:15:9: &c escapes to heap
./main.go:12:6: moved to heap: c
./main.go:20:19: dummy() escapes to heap
./main.go:20:13: main ... argument does not escape
&{}

注意第 4 行出现了新的提示:将 c 移到堆中。这句话表示,Go 编译器已经确认如果将 c 变量分配在栈上是无法保证程序最终结果的。如果坚持这样做,dummy() 的返回值将是 Data 结构的一个不可预知的内存地址。这种情况一般是 C/C++ 语言中容易犯错的地方:引用了一个函数局部变量的地址。

Go 语言最终选择将 c 的 Data 结构分配在堆上。然后由垃圾回收器去回收 c 的内存。

原则

在使用 Go 语言进行编程时,Go 语言的设计者不希望开发者将精力放在内存应该分配在栈还是堆上的问题。编译器会自动帮助开发者完成这个纠结的选择。但变量逃逸分析也是需要了解的一个编译器技术,这个技术不仅用于 Go 语言,在 Java 等语言的编译器优化上也使用了类似的技术。

编译器觉得变量应该分配在堆和栈上的原则是:
变量是否被取地址。
变量是否发生逃逸。

总结
一个局部变量被取地址并返回出去的话就会被分配到堆上,如果只是返回局部变量那么还是会被分配到栈上

Go语言常量和const关键字

相对于变量,常量是恒定不变的值,例如圆周率。

可以在编译时,对常量表达式进行计算求值,并在运行期使用该计算结果,计算结果无法被修改。

常量表示起来非常简单,如下面的代码:

1
2
const pi = 3.141592
const e = 2.718281

常量的声明和变量声明非常类似,只是把 var 换成了 const

多个变量可以一起声明,类似的,常量也是可以多个一起声明的,如下面的代码:

1
2
3
4
const (
pi = 3.141592
e = 2.718281
)

常量因为在编译期确定,所以可以用于数组声明,如下面的代码:

1
2
const size = 4
var arr [size]int

Go语言模拟枚举(const和iota模拟枚举)

Go 语言中现阶段没有枚举,可以使用 const 常量配合 iota 模拟枚举,请看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
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)

代码输出如下:

1
2
1 2 3 4
4

代码说明如下:
第 1 行中将 int 定义为 Weapon 类型,就像枚举类型其实本质是一个 int 一样。当然,某些情况下,如果需要 int32 和 int64 的枚举,也是可以的。

第3行中,将 Array 常量的类型标识为 Weapon,这样标识后,const 下方的常量可以是默认类型的,默认时,默认使用前面指定的类型作为常量类型。该行使用 iota 进行常量值自动生成。iota 起始值为 0,一般情况下也是建议枚举从 0 开始,让每个枚举类型都有一个空值,方便业务和逻辑的灵活使用。

一个 const 声明内的每一行常量声明,将会自动套用前面的 iota 格式,并自动增加。这种模式有点类似于电子表格中的单元格自动填充。只需要建立好单元格之间的变化关系,拖动右下方的小点就可以自动生成单元格的值。

当然,iota 不仅只生成每次增加 1 的枚举值。我们还可以利用 iota 来做一些强大的枚举常量值生成器。下面的代码可以方便生成标志位常量:

1
2
3
4
5
6
7
8
const (
FlagNone = 1 << iota
FlagRed
FlagGreen
FlagBlue
)
fmt.Printf("%d %d %d\n", FlagRed, FlagGreen, FlagBlue)
fmt.Printf("%b %b %b\n", FlagRed, FlagGreen, FlagBlue)

代码输出如下:

1
2
2 4 8
10 100 1000

在代码中编写一些标志位时,我们往往手动编写常量值,如果常量值特别多时,很容易重复或者写错。因此,使用 ioto 自动生成较为方便。

代码说明如下:
第 2 行中 iota 使用了一个移位操作,每次将上一次的值左移一位,以做出每一位的常量值。
第 7 行,将 3 个枚举按照常量输出,分别输出 2、4、8,都是将 1 每次左移一位的结果。
第 8 行,将枚举值按二进制格式输出,可以清晰地看到每一位的变化。

将枚举值转换为字符串

枚举在 C# 语言中是一个独立的类型,可以通过枚举值获取值对应的字符串。例如,C# 中 Week 枚举值 Monday 为 1,那么可以通过 Week.Monday.ToString() 函数获得 Monday 字符串。

Go 语言中也可以实现这一功能,见下面的例子。
转换字符串:

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"
// 声明芯片类型
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)
}

运行结果:

1
CPU 1

代码说明如下:
第 4 行,将 int 声明为 ChipType 芯片类型。
第 6 行,将 const 里定义的一句常量值设为 ChipType 类型,且从 0 开始,每行值加 1。
第 10 行,定义 ChipType 类型的方法 String(),返回字符串。
第 11~20 行,使用 switch 语句判断当前的 ChitType 类型的值,返回对应的字符串。
第 23 行,输出 CPU 的值并按整型格式输出。

使用 String() 方法的 ChipType 在使用上和普通的常量没有区别。当这个类型需要显示为字符串时,Go 语言会自动寻找 String() 方法并进行调用。

Go语言type关键字(类型别名)

注意:本节内容涉及 Go 语言新版本的功能。内容上会涉及后续章节讲解的类型定义及结构体嵌入等特性。另外,本节内容适用于对 Go 语言很熟悉且正在关注工程升级、代码重构等问题的读者阅读。

类型别名是 Go 1.9 版本添加的新功能。主要用于代码升级、迁移中类型的兼容性问题。在 C/C++ 语言中,代码重构升级可以使用宏快速定义新的一段代码。Go 语言中没有选择加入宏,而是将解决重构中最麻烦的类型名变更问题。

在 Go 1.9 版本之前的内建类型定义的代码是这样写的:

1
2
type byte uint8
type rune int32

而在 Go 1.9 版本之后变为:

1
2
type byte = uint8
type rune = int32

这个修改就是配合类型别名而进行的修改。

区分类型别名与类型定义

类型别名的写法为:

1
type TypeAlias = Type

类型别名规定:TypeAlias 只是 Type 的别名,本质上 TypeAliasType 是同一个类型。就像一个孩子小时候有小名、乳名,上学后用学名,英语老师又会给他起英文名,但这些名字都指的是他本人。

类型别名与类型定义表面上看只有一个等号的差异,那么它们之间实际的区别有哪些呢?下面通过一段代码来理解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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)
}

代码运行结果:

1
2
a type: main.NewInt
a2 type: int

代码说明如下:
第 6 行,将 NewInt 定义为 int 类型,这是常见定义类型的方法,通过 type 关键字的定义,NewInt 会形成一种新的类型。NewInt 本身依然具备int的特性。
第 8 行,将 IntAlias 设置为 int 的一个别名,使用 IntAlias 与 int 等效。
第 11 行,将 a 声明为 NewInt 类型,此时若打印,则 a 的值为 0。
第 13 行,使用%T格式化参数,显示 a 变量本身的类型。
第 15 行,将 a2 声明为 IntAlias 类型,此时打印 a2 的值为 0。
第 17 行,显示 a2 变量的类型。

结果显示a的类型是 main.NewInt,表示 main 包下定义的 NewInt 类型。a2 类型是 int。IntAlias 类型只会在代码中存在,编译完成时,不会有 IntAlias 类型。

非本地类型不能定义方法

能够随意地为各种类型起名字,是否意味着可以在自己包里为这些类型任意添加方法?参见下面的代码演示:

1
2
3
4
5
6
7
8
9
10
11
package main
import (
"time"
)
// 定义time.Duration的别名为MyDuration
type MyDuration = time.Duration
// 为MyDuration添加一个函数
func (m MyDuration) EasySet(a string) {
}
func main() {
}

代码说明如下:
第 6 行,使用类型别名为 time.Duration 设定一个别名叫 MyDuration。
第 8 行,为这个别名添加一个方法。

编译上面代码报错,信息如下:

1
cannot define new methods on non-local type time.Duration

编译器提示:不能在一个非本地的类型 time.Duration 上定义新方法。非本地方法指的就是使用 time.Duration 的代码所在的包,也就是 main 包。因为 time.Duration 是在 time 包中定义的,在 main 包中使用。time.Duration 包与 main 包不在同一个包中,因此不能为不在一个包中的类型定义方法。

解决这个问题有下面两种方法:
将第 6 行修改为 type MyDuration time.Duration,也就是将 MyDuration 从别名改为类型。
将 MyDuration 的别名定义放在 time 包中。

在结构体成员嵌入时使用别名

当类型别名作为结构体嵌入的成员时会发生什么情况?请参考下面的代码。

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
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())
}
}

代码输出如下:

1
2
FieldName: FakeBrand, FieldType: Brand
FieldName: Brand, FieldType: Brand

代码说明如下:
第 7 行,定义商标结构。
第 13 行,为商标结构添加 Show() 方法。
第 10 行,为 Brand 定义一个别名 FakeBrand。
第 13~17 行,定义车辆结构 Vehicle,嵌入 FakeBrand 和 Brand 结构。
第 22 行,将 Vechicle 实例化为 a。
第 24 行,显式调用 Vehicle 中 FakeBrand 的 Show() 方法。
第 26 行,使用反射取变量 a 的反射类型对象,以查看其成员类型。
第 28~32 行,遍历 a 的结构体成员。
第 33 行,打印 Vehicle 类型所有成员的信息。

这个例子中,FakeBrand 是 Brand 的一个别名。在 Vehicle 中嵌入 FakeBrand 和 Brand 并不意味着嵌入两个 Brand。FakeBrand 的类型会以名字的方式保留在 Vehicle 的成员中。

如果尝试将第 24 行改为:

1
a.Show()

编译器将发生报错:

1
ambiguous selector a.Show

在调用 Show() 方法时,因为两个类型都有 Show() 方法,会发生歧义,证明 FakeBrand 的本质确实是 Brand 类型。

附加链接:
Go结构体与方法
Go语言type关键字
Go语言中type的几种使用

Go语言容器(container)

变量在一定程度上能满足函数及代码要求。如果编写一些复杂算法、结构和逻辑,就需要更复杂的类型来实现。这类复杂类型一般情况下具有各种形式的存储和处理数据的功能,将它们称为“容器(container)”。

在很多语言里,容器是以标准库的方式提供,你可以随时查看这些标准库的代码,了解如何创建,删除,维护内存。

本章将以实用为目的,详细介绍数组、切片、映射,以及列表的增加、删除、修改和遍历的使用方法。本章既可以作为教程,也可以作为字典,以方便开发者日常的查询和应用。
其它语言中的容器
C语言没有提供容器封装,开发者需要自己根据性能需求进行封装,或者使用第三方提供的容器。
C++ 语言的容器通过标准库提供,如 vector 对应数组,list 对应双链表,map 对应映射等。
C# 语言通过 .NET 框架提供,如 List 对应数组,LinkedList 对应双链表,Dictionary 对应映射。
Lua 语言的 table 实现了数组和
映射的功能,Lua 语言默认没有双链表支持。

Go语言数组详解

数组(Array)是一段固定长度的连续内存区域。

在 Go 语言中,数组从声明时就确定,使用时可以修改数组成员,但是数组大小不可变化。

C/C++ 中的数组

C语言和 Go 语言中的数组概念完全一致。C语言的数组也是一段固定长度的内存区域,数组的大小在声明时固定下来。下面演示一段C语言的数组:

1
2
int a[10]={ 0,1,2,3,4,5,6,7,8,9 };
int b[4];

此时,a 和 b 类型都是 int*,也就是整型指针。而C语言中,也可以使用 malloc() 函数动态地分配一段内存区域。C++ 语言中可以使用 new() 函数。例如:

1
2
int* a = (int*)malloc(10);
int* b = new int(4);

此时,a 和 b 的类型也是 int*。a 和 b 此时分配内存的方式类似于 Go 语言的切片。

Go 的数组和切片都是从C语言延续过来的设计。

Go 语言数组的声明

数组的写法如下:

1
var 数组变量名 [元素数量]T

其中:
数组变量名:数组声明及使用时的变量名。
元素数量:数组的元素数量。可以是一个表达式,但最终通过编译期计算的结果必须是整型数值。也就是说,元素数量不能含有到运行时才能确认大小的数值。
T 可以是任意基本类型,包括 T 为数组本身。但类型为数组本身时,可以实现多维数组。

下面是一段数组的演示例子:

1
2
3
4
5
var team [3]string
team[0] = "hammer"
team[1] = "soldier"
team[2] = "mum"
fmt.Println(team)

输出结果:

1
[hammer soldier mum]

代码说明如下:
第 1 行,将 team 声明为包含 3 个元素的字符串数组。
第 2~4 行,为 team 的元素赋值。

Go语言数组的初始化

数组可以在声明时使用初始化列表进行元素设置,参考下面的代码:

1
var team = [3]string{"hammer", "soldier", "mum"}

这种方式编写时,需要保证大括号后面的元素数量与数组的大小一致。但一般情况下,这个过程可以交给编译器,让编译器在编译时,根据元素个数确定数组大小。

1
var team = [...]string{"hammer", "soldier", "mum"}

…表示让编译器确定数组大小。上面例子中,编译器会自动为这个数组设置元素个数为 3。

遍历数组——访问每一个数组元素

遍历数组也和遍历切片类似,看下面代码:

1
2
3
4
5
6
7
var team [3]string
team[0] = "hammer"
team[1] = "soldier"
team[2] = "mum"
for k, v := range team {
fmt.Println(k, v)
}

代码输出结果:

1
2
3
hammer
soldier
mum

代码说明如下:
第 5 行,使用 for 循环,遍历 team 数组,遍历出的键 k 为数组的索引,值 v 为数组的每个元素值。
第 6 行,将每个键值打印出来。

Go语言切片详解

切片(Slice)是一个拥有相同类型元素的可变长度的序列。Go 语言切片的内部结构包含地址、大小和容量。切片一般用于快速地操作一块数据集合。如果将数据集合比作切糕的话,切片就是你要的“那一块”。切的过程包含从哪里开始(这个就是切片的地址)及切多大(这个就是切片的大小)。容量可以理解为装切片的口袋大小,如下图所示。

图片消失啦
切片结构和内存分配

从数组或切片生成新的切片

切片默认指向一段连续内存区域,可以是数组,也可以是切片本身。

从连续内存区域生成切片是常见的操作,格式如下:

1
slice [开始位置:结束位置]

slice 表示目标切片对象。
开始位置对应目标切片对象的索引。
结束位置对应目标切片的结束索引。

从数组生成切片,代码如下:

1
2
var a  = [3]int{1, 2, 3}
fmt.Println(a, a[1:2])

a 是一个拥有 3 个整型元素的数组,被初始化数值 1 到 3。使用 a[1:2] 可以生成一个新的切片。代码运行结果如下:

1
[1 2 3] [2]

[2] 就是 a[1:2] 切片操作的结果。

从数组或切片生成新的切片拥有如下特性:
取出的元素数量为:结束位置-开始位置。
取出元素不包含结束位置对应的索引,切片最后一个元素使用 slice[len(slice)] 获取。
当缺省开始位置时,表示从连续区域开头到结束位置。
当缺省结束位置时,表示从开始位置到整个连续区域末尾。
两者同时缺省时,与切片本身等效。
两者同时为0时,等效于空切片,一般用于切片复位。

根据索引位置取切片 slice 元素值时,取值范围是(0~len(slice)-1),超界会报运行时错误。生成切片时,结束位置可以填写 len(slice) 但不会报错。

下面在具体的例子中熟悉切片的特性。
1) 从指定范围中生成切片
切片和数组密不可分。如果将数组理解为一栋办公楼,那么切片就是把不同的连续楼层出租给使用者。出租的过程需要选择开始楼层和结束楼层,这个过程就会生成切片。示例代码如下:

1
2
3
4
5
6
7
8
9
10
var highRiseBuilding [30]int
for i := 0; i < 30; i++ {
highRiseBuilding[i] = i + 1
}
// 区间
fmt.Println(highRiseBuilding[10:15])
// 中间到尾部的所有元素
fmt.Println(highRiseBuilding[20:])
// 开头到中间的所有元素
fmt.Println(highRiseBuilding[:2])

代码输出如下:

1
2
3
[11 12 13 14 15]
[21 22 23 24 25 26 27 28 29 30]
[1 2]

代码中构建了一个 30 层的高层建筑。数组的元素值从 1 到 30,分别代表不同的独立楼层。输出的结果是不同租售方案。

代码说明如下:
第 8 行,尝试出租一个区间楼层。
第 11 行,出租 20 层以上。
第 14 行,出租 2 层以下,一般是商用铺面。

切片有点像C语言里的指针。指针可以做运算,但代价是内存操作越界。切片在指针的基础上增加了大小,约束了切片对应的内存区域,切片使用中无法对切片内部的地址和大小进行手动调整,因此切片比指针更安全、强大。
2) 表示原有的切片
生成切片的格式中,当开始和结束都范围都被忽略,则生成的切片将表示和原切片一致的切片,并且生成的切片与原切片在数据内容上是一致的,代码如下:
a := []int{1, 2, 3}
fmt.Println(a[:])
a 是一个拥有 3 个元素的切片。将 a 切片使用 a[:] 进行操作后,得到的切片与 a 切片一致,代码输出如下:
[1 2 3]

3) 重置切片,清空拥有的元素
把切片的开始和结束位置都设为 0 时,生成的切片将变空,代码如下:
a := []int{1, 2, 3}
fmt.Println(a[0:0])
代码输出如下:
[]

直接声明新的切片
除了可以从原有的数组或者切片中生成切片,你也可以声明一个新的切片。每一种类型都可以拥有其切片类型,表示多个类型元素的连续集合。因此切片类型也可以被声明。切片类型声明格式如下:
var name []T

name 表示切片类型的变量名。
T 表示切片类型对应的元素类型。

下面代码展示了切片声明的使用过程:
// 声明字符串切片
var strList []string
// 声明整型切片
var numList []int
// 声明一个空切片
var numListEmpty = []int{}
// 输出3个切片
fmt.Println(strList, numList, numListEmpty)
// 输出3个切片大小
fmt.Println(len(strList), len(numList), len(numListEmpty))
// 切片判定空的结果
fmt.Println(strList == nil)
fmt.Println(numList == nil)
fmt.Println(numListEmpty == nil)
代码输出结果:
[] [] []
0 0 0
true
true
false

代码说明如下:
第 2 行,声明一个字符串切片,切片中拥有多个字符串。
第 5 行,声明一个整型切片,切片中拥有多个整型数值。
第 8 行,将 numListEmpty 声明为一个整型切片。本来会在{}中填充切片的初始化元素,这里没有填充,所以切片是空的。但此时 numListEmpty 已经被分配了内存,但没有元素。
第 11 行,切片均没有任何元素,3 个切片输出元素内容均为空。
第 14 行,没有对切片进行任何操作,strList 和 numList 没有指向任何数组或者其他切片。
第 17 行和第 18 行,声明但未使用的切片的默认值是 nil。strList 和 numList 也是 nil,所以和 nil 比较的结果是 true。
第 19 行,numListEmpty 已经被分配到了内存,但没有元素,因此和 nil 比较时是 false。

切片是动态结构,只能与nil判定相等,不能互相判等时。

声明新的切片后,可以使用 append() 函数来添加元素。
使用 make() 函数构造切片
如果需要动态地创建一个切片,可以使用 make() 内建函数,格式如下:
make( []T, size, cap )

T:切片的元素类型。
size:就是为这个类型分配多少个元素。
cap:预分配的元素数量,这个值设定后不影响 size,只是能提前分配空间,降低多次分配空间造成的性能问题。

示例如下:
a := make([]int, 2)
b := make([]int, 2, 10)
fmt.Println(a, b)
fmt.Println(len(a), len(b))
代码输出如下:
[0 0] [0 0]
2 2

a 和 b 均是预分配 2 个元素的切片,只是 b 的内部存储空间已经分配了 10 个,但实际使用了 2 个元素。

容量不会影响当前的元素个数,因此 a 和 b 取 len 都是 2。
温馨提示
使用 make() 函数生成的切片一定发生了内存分配操作。但给定开始与结束位置(包括切片复位)的切片只是将新的切片结构指向已经分配好的内存区域,设定开始与结束位置,不会发生内存分配操作。

切片不一定必须经过 make() 函数才能使用。生成切片、声明后使用 append() 函数均可以正常使用切片。

待更新


本文标题:Go学习笔记

文章作者:游戏人生

发布时间:2019年02月13日 - 16:02

最后更新:2020年12月27日 - 12:12

原始链接:http://www.tjl-myblog.cn/Go学习笔记.html

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

-------------本文结束感谢您的阅读-------------