| 导语 为了充实生活,保持一种爱学习的态度,维持学习能力,将开始学习Go语言及其生态链新知识。
1 Go语言简介
现今,多核CPU已成为服务器的标配,但是对多核的运算能力挖掘一直由程序员人工设计算法及框架来完成。这个过程需要开发人员具有一定的并发设计及框架设计能力。
虽然一些编程语言的框架在不断地提高多核资源使用效率,如Java的Netty等,但仍然需要开发人员花费大量的时间和精力搞懂这些框架的运行原理后才能熟练掌握。
Go
语言在多核并发上拥有原生的设计优势。Go
语言从2009年11月开源,2012年发布1.0稳定版本以来,已经拥有活跃的社区和全球众多开发者,并且与Swift一样,成为当前非常流行的开发语言之一。很多公司,特别是中国的互联网公司,即将或者已经完成了使用Go
语言改造旧系统的过程。经过Go
语言重构的系统能使用更少的硬件资源而有更高的并发和I/O吞吐表现。
Go
语言是Google公司开发的一种静态型、编译型并自带垃圾回收和并发的编程语言。Go
语言在语言层可以通过goroutine
对函数实现并发执行。goroutine
类似于线程但是并非线程,goroutine
会在Go
语言运行时进行自动调度,因此,Go
语言非常适合用于高并发网络服务的编写。
Go
语言上手容易,实现一个简单的HTTP服务器只需要几行代码:
package main // 标记当前文件为main包,main包是Go程序的入口包
import (
"net/http" // 导入包含HTTP基础封装和访问的包
)
func main() { // 入口函数
// 文件服务器将当前目录作为根目录
http.Handle("/", http.FileServer(http.Dir(".")))
http.ListenAndServe(":8080", nil)
}
运行程序:
go run httpserver.go
在浏览器输入http://127.0.0.1:8080
或者http://localhost:8080
即可浏览文件。
Go
语言的代码可以直接输出为目标平台的原生可执行文件,不使用虚拟机,只有运行时提供垃圾回收和goroutine调度等。
Go
语言使用自己的链接器,不依赖任何系统提供的编译器、链接器,因此编译出的可执行文件可以直接运行在几乎所有的操作系统和环境中。
Go
1.5版本之后,实现自举——即用Go
语言编写Go
语言编译器及所有工具链的功能。
将Go
代码编译出可执行文件:
go build ./helloworld.go
Go
语言不仅可以输出可执行文件,还可以编译输出能导入C语言的静态库、动态库。从1.7版本开始,Go
语言支持将代码编译为插件,使用插件可以动态加载需要的模块,而不是一次性将所有的代码编译为一个可执行文件。
Go
语言工程结构简单,源码无需头文件,编译的文件都是go文件,无需解决方案、工程文件和MakeFile。
Go
语言编译速度快,可以利用自己的特性实现并发编译,并发编译的最小元素是包。从Go
1.9版本开始,最小冰饭编译元素缩小到函数,整体编译速度提高了20%。另外,语言语法简单,具有严谨的工程结构设计,没有头文件,不允许包的交叉依赖,在很大程度上加速了编译的过程。
Go
语言原生支持并发,从语言层原生支持并发,无须第三方库、开发者的编程技巧及开发经验就可以轻松地在Go
语言运行时来帮助开发者决定怎么使用CPU资源。
Go
语言的并发是基于goroutine
。goroutine类似于线程,但并非线程,可以将其理解为一种虚拟线程。Go
语言运行时会参与调度goroutine,并将goroutine合理地分配到每个CPU中,最大限度地使用CPU性能。
多个goroutine中,Go
语言使用通道channel
进行通信,可以将需要并发的程序设计为生产者和消费者模式,将数据放入通道。通道的另一端将数据进行并发计算并返回结果。
graph LR
A[producer] -->|channel| B[consumer]
package main
import (
"fmt" // 格式化
"math/rand" // 随机数
"time" // 时间
)
// 数据生产者(传入只能写入的通道)
func producer(header string, channel chan <- string) {
// 无限循环,不断产生数据
for {
// 将随机数和字符串格式化为字符串发送给通道
channel <- fmt.Sprintf("%s: %v", header, rand.Int31())
// 等待1秒
time.Sleep(time.Second) // 暂停1s,不会影响其他routine
}
}
// 数据消费者(传入只能写入的通道)
func customer(channel <- chan string) {
// 不停获取数据
for {
// 从通道中取出数据,此处会阻塞直到信道中返回数据
message := <- channel
fmt.Println(message)
}
}
func main () {
// 创建一个字符串类型的通道
channel := make(chan string)
// 创建producer()函数的并发goroutine
go producer("cat", channel)
go producer("dog", channel)
// 数据消费函数
customer(channel)
}
以上整段代码中,没有线程创建,没有线程池也没有加锁,仅仅通过go关键字实现goroutine,并用通道实现数据交换。
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 | Go1.7加入的插件系统,支持将代码编译为插件,按需加载 |
reflect | 语言反射支持,可以动态获得代码中的类型信息,获取和修改变量的值 |
regexp | 正则表达式封装 |
runtime | 运行时接口 |
sort | 排序接口 |
strings | 字符串转换、解析及实用函数 |
time | 时间接口 |
text | 文本模板及Token语法器 |
Go
语言将C语言中较为容易发生错误的写法进行调整,做出相应的编译提示。
(1)去掉循环冗余括号
C语言中的循环
for (int a = 0; a < 10; ++a) {
//
}
在Go
语言中变成
for a := 0; a < 10; a++ {
//
}
(2)去掉表达式冗余括号
C语言中的判断语句
if (expression) {
//
}
在Go
语言中,无需添加表达式括号
if expression {
//
}
(3)强制的代码风格
Go
语言中,左括号必须紧接着语句不换行。
此外,自增操作符,只有一种写法
i++
,没有前置自增
2. 使用Go语言的项目
Go
语言的简单、高效、并发特性吸引了众多传统语言开发者的加入。
2.1 Docker项目
Docker是一种操作系统层面的虚拟化技术,可以在操作系统和应用程序之间进行隔离,也可以称为容器。Docker可以在一台物理服务器上快速运行一个或多个实例。例如,启动一个CentOS系统,并在其内部执行命令后结束,整个过程就像在自己的操作系统执行一样高效。
2.2 Golang项目
Go语言的早期源码使用C语言和汇编语言写成。从1.5版本实现自举后,完全使用Go语言自身进行编写。Go语言的源码对于了解Go语言的底层调度有极大的参考意义。
2.3 Kubernetes项目
Kubernetes是Google公司开发的构建于Docker之上的容器调度服务,用户可以通过Kubernetes集群进行云端容器集群管理。
2.4 etcd项目
etcd是一款分布式、可靠的KV存储系统,可以快速进行云配置。
2.5 beego项目
beego类似Python的Tornado框架,采用了RESTFul的设计思路,是使用Go语言编写的一个极轻量级、高可伸缩性和高性能的Web应用框架。
2.6 martini项目
martini是一款快速构建模块化的Web应用的Web框架。
2.7 codis项目
codis是国产的优秀分布式Redis解决方案。
2.8 delve项目
delve是Go语言强大的调试器,被很多集成环境和编辑器整合。
3 Go语言的基本语法与使用
个人觉得不用将所有的Go语言特性全部介绍,相反,我会从C++开发者的角度列出某些具有差异性的特性,并且注重写出例子,从例子中体会语法的异同,这样能节省篇幅,突出重点。
3.1 变量
变量的功能是存储用户的数据,与C++一样,Go语言的每一个变量都拥有自己的类型,必须经过声明才能使用。
声明变量
变量声明形式为:var 变量名 变量类型
,行尾无需分号
// 整型类型变量
var a int
// 字符串类型变量
var b string
// 32位浮点切片类型变量(浮点切片表示由多个浮点类型组成的数据结构)
var c []float32
// 返回值为bool的函数变量(一般用于回调,在需要时调用以变量形式存储的函数)
var d func() bool
// 结构体类型
var e struct {
x int
}
批量形式
var (
a int
b string
c []float32
d func() bool
e struct {
x int
}
)
变量的初始化
Go语言在声明变量时,自动对变量对应的内存区域进行初始化操作,每个变量会初始化为其类型的默认值
- 整型和浮点型默认值为0
- 字符串类型默认值为空字符串
- 布尔型默认值为false
- 切片、函数、指针类型的默认值为nil
当然,依然可以在声明变量的时候赋予变量一个初始值
在C++中,变量在声明时,并不会对其对应内存区域进行清理操作,例如VC的烫烫烫和屯屯屯
var 变量名 类型 = 表达式
var hp int = 100
var attack = 40
var defence = 20
var damageRate float32 = 0.17 // 默认不指出类型,使用float64精度
var damage = float32(attack - defence) * damageRate
hp := 100
左值变量必须是没有定义过的变量,若定义过,会发生编译错误。
由于精简,短变量声明并初始化在开发中使用比较普遍:
// 普通情况
var conn net.Conn
var err error
conn, err = net.Dial("tcp", "127.0.0.1:8080")
// 短变量
conn, err := net.Dial("tcp", "127.0.0.1:8080")
net.Dial
按指定协议和地址发起网络连接
至少有一个新声明的变量出现左值中,这时即使其他变量名可能是重复声明的,编译器也不会报错:
conn, err := net.Dial("tcp", "127.0.0.1:8080")
conn2, err := net.Dial("tcp", "127.0.0.1:8080")
多变量同时赋值
var a int = 100
var b int = 200
b, a = a, b // 与Python中的用法一致
多重赋值时:
(1)变量的左值和右值按照从左到右的顺序赋值
(2)在Go语言的错误处理和函数返回值中会大量使用
匿名变量
使用多重赋值时,如果不需要在左值中接收变量,可以使用匿名变量,表示丢弃
func GetData() (int, int) {
return 100, 200
}
a, _ := GetData()
_, b := GetData()
在Lua等其他语言中,匿名变量叫做哑元变量,它不占用命名空间,也不会分配内存。
3.2 数据类型
- 基本类型:整型、浮点型、布尔型、字符串类型
- 切片,比指针安全的高效内存操作结构
- 结构体
- 函数
- map
- 通道(channel),与并发息息相关
整型
int8
、uint8
、int16
、uint16
、int32
、uint32
、int64
、uint64
uint8就是byte类型,int16是C++中的short,int64是C++中的long
Go语言中也有自动匹配特定平台整型长度的类型——int和uint
浮点型
float32
:最大值3.4e38
,math.MaxFloat32
float64
:最大值1.8e308
,math.MaxFloat64
布尔型
只有true和false两个值
Go语言不允许将整数强制转换为布尔型
布尔型无法参与数值运算,也无法与其他类型进行转换
字符串
字符串在Go语言中以原生数据类型出现
C++和C#等语言中,字符串以类的方式进行封装
str := "hello world"
ch := "中文"
字符实现基于UTF-8
Go语言的字符串内部实现使用UTF-8
编码,通过rune
类型,可以对每个UTF-8
字符进行访问。
Go语言也支持像C++
中的按ASCII
码方式进行逐字符访问。
多行字符串
所有的转义字符均无效
const str = `first line
second line
third line
\r\n
`
字符
Go语言中的字符有两种:
(1)uint8
类型,或者byte
型,代表了ASCII码的一个字符
(2)rune
类型,代表一个UTF-8字符,实际上是一个int32
类型
使用fmt.Printf
中的%T
可以输出变量的实际类型
var a byte = 'a'
fmt.Printf("%d %T\n", a, a) // 输出97 uint8
var b rune = '你'
fmt.Printf("%d %T\n", b, b) // 输出20320 int32
UTF-8和Unicode的区别
(1)Unicode和ASCII一样,是一种字符集
(2)UTF-8是一种编码规则,将Unicode中字符的ID以某种方式进行编码
UTF-8是一种变长编码规则,从1到4个字节不等
切片
切片能动态分配空间,是一个拥有相同类型元素的可变长度的序列
var name []T
// T是切片类型,可以是整型、浮点型、布尔型、切片、map、函数等
切片元素使用[]
进行访问
a := make([]int, 3)
a[0] = 1
a[1] = 2
a[2] = 3
切片可以在其元素集合内连续地选取一段区域作为新的切片(与Python类型)
字符串也可以按切片的方式进行操作
str := "hello world"
fmt.Println(str[6:])
3.3 类型转换
Go语言使用前置类型+括号的方式进行类型转换T(表达式)
类型转换时,需要考虑两种类型的关系和范围,是否会发生数值截断等
fmt.Println("int8", math.MinInt8, math.MaxInt8)
fmt.Println("int16", math.MinInt16, math.MaxInt16)
fmt.Println("int32", math.MinInt32, math.MaxInt32)
fmt.Println("int64", math.MinInt64, math.MaxInt64)
3.4 指针
- 类型指针:(1)允许对指针类型指向的数据进行修改;(2)传递数据使用指针,可以避免拷贝;(3)不能进行偏移和运算;
- 切片:由指向起始元素的原始指针、元素数量和容量组成
对比C/C++,Go语言的指针类型变量拥有指针的高效访问,但又不会发生指针偏移,从而避免非法修改关键性数据问题;同时,垃圾回收也比较容易对不会发生偏移的指针进行检索和回收。
切片比原始指针具备更强大的特性,更为安全。切片发生越界时,运行时会报出宕机并打出堆栈,而原始指针只会崩溃。
每个变量在运行时都拥有一个地址,代表变量在内存中的位置
ptr := &v // ptr的类型是“*T”
变量、指针、地址:每个变量都拥有地址,指针的值就是地址
var cat int = 1
var str string = "banana"
fmt.Printf("%p %p", &cat, &str) // 带有0x前缀的十六进制地址
变量、地址、指针变量、取地址、取值
- 对变量进行取地址(&)操作,可以获得变量的指针变量
- 指针变量的值是指针地址
- 对指针变量进行取值(*)操作,可以获得指针变量指向的原变量的值
使用指针修改值
func swap(a, b *int) {
t := *a
*a = *b
*b = t
}
使用指针变量获取命令行的输入信息
package main
import (
"flag"
"fmt"
)
var mode = flag.String("mode", "", "process mode")
func main() {
// 解析命令行参数
flag.Parse()
// 输出命令行参数
fmt.Println(*mode)
}
其中,定义mode变量的参数含义是:
- 参数名称
- 参数默认值
- 参数说明:使用—help时,会出现在说明中
使用命令行传入参数
go run flagparse.go --mode=fast
创建指针的另一种方法——new()函数
str := new(string) // 初始化为默认值
*str = "ninja"
fmt.Println(*str)
3.5 变量生命周期
堆分配内存和栈分配内存相比,堆适合不可预知大小的内存分配,但是分配速度较慢、而且会形成碎片。
堆和栈各有优缺点,在C/C++语言中,需要开发者决定如何进行内存分配,选用怎样的内存分配方式来适应不同的算法需求。Go语言将这个过程整合到编译器中,叫做”变量逃逸分析(Escape Analysis)“,自动决定变量分配方式,提高运行效率。这个技术由编译器分析代码的特征和代码生命期,决定应该在堆还是栈中进行内存分配。
逃逸分析
package main
import "fmt"
func dummy(b int) int {
var c int
c = b
return c
}
func void() {}
func main() {
var a int
void()
fmt.Println(a, dummy(0))
}
运行命令
go run -gcflags "-m -l" escape1.go
-gcflags
是编译参数,其中-m
表示进行内存分配分析,-l
表示避免程序内联
# command-line-arguments
.\escape1.go:16:13: a escapes to heap
.\escape1.go:16:22: dummy(0) escapes to heap
.\escape1.go:16:13: main ... argument does not escape
0 0
变量a逃逸到堆,dummy()返回的值逃逸到堆,它们被fmt.Println使用后还是会在其声明后继续再main函数中存在。而c变量即使分配的内存被释放,也不会影响main中dummy返回的值,它使用栈分配不会影响结果。
取地址发生逃逸
package main
import "fmt"
type Data struct {}
func Dummy() *Data {
var c Data
return &c
}
func main() {
fmt.Println(Dummy())
}
分析结果
# command-line-arguments
.\escape2.go:10:9: &c escapes to heap
.\escape2.go:9:6: moved to heap: c
.\escape2.go:14:19: Dummy() escapes to heap
.\escape2.go:14:13: main ... argument does not escape
&{}
取函数局部变量c的地址并返回,Go语言允许这么做
c移动到堆,表示,Go编译器已经确认如果将c变量分配到栈上是无法保证程序最终结果的。
Go语言最终选择将c的Data结构分配到堆上,然后由垃圾回收器去回收c的内存。
编译器决定变量分配在堆和栈上的原则
- 变量是否被取地址
- 变量是否发生逃逸
在使用Go语言进行编程时,Go语言的设计者不希望开发者将精力放在内存应该怎么分配上,编译器会自动帮助开发者完成这个堆和栈的选择。
这个技术不仅用于Go语言,在Java等语言的编译器优化上也使用了类似的技术
3.6 字符串应用
计算字符串长度
- ASCII字符串长度使用
len()
函数 - Unicode字符串长度使用
utf8.RuneCountInString()
函数
func main() {
tip1 := "genji is a ninja"
fmt.Println(len(tip1)) // 16
tip2 := "忍者"
fmt.Println(len(tip2)) // 6
fmt.Println(utf8.RuneCountInString("忍者")) // 2
fmt.Println(utf8.RuneCountInString("龙刃出鞘,fight!")) // 11
}
len()函数的返回值的类型为int,表示字符串的ASCII字符个数或字节长度
Go语言中的字符串都以UTF-8格式保存,每个中文占用3个字节
遍历字符串
获取每一个字符
theme := "狙击 start"
for i := 0; i < len(theme); i++ {
fmt.Printf("ascii: %c %d\n", theme[i], theme[i])
}
由于没有使用Unicode,汉字被显示为乱码。
for _, s := range theme {
fmt.Printf("Unicode: %c %d\n", s, s)
}
总结:
- ASCII字符串遍历直接使用下标
- Unicode字符串遍历使用range
获取字符串子串
// 获取第二个死神后面的子串
tracer := "死神来了,死神bye bye"
comma := strings.Index(tracer, ",")
pos := strings.Index(tracer[comma:], "死神")
fmt.Println(comma, pos, tracer[comma + pos:]) // 12 3 死神bye bye
总结:
- strings.Index——正向搜索子串
- strings.LastIndex——反向搜索子串
- 搜索的起始位置可以通过切片偏移制作
fmt.Println(tracer[strings.LastIndex(tracer, "死神"):])
也是可以的!!