| 导语 为了充实生活,保持一种爱学习的态度,维持学习能力,将开始学习Go语言及其生态链新知识。

1 Go语言简介

现今,多核CPU已成为服务器的标配,但是对多核的运算能力挖掘一直由程序员人工设计算法及框架来完成。这个过程需要开发人员具有一定的并发设计及框架设计能力。

虽然一些编程语言的框架在不断地提高多核资源使用效率,如Java的Netty等,但仍然需要开发人员花费大量的时间和精力搞懂这些框架的运行原理后才能熟练掌握。

GoGoGoGo
GoGogoroutinegoroutinegoroutineGoGo
Go
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:8080http://localhost:8080
Go
Go
GoGoGo
Go
go build ./helloworld.go
GoGo
Go
GoGo
GoGo
GogoroutineGo
Gochannel
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格式化操作
gogo语言的语法、语法树、类型等(可以通过这个包进行代码信息提取和修改)
htmlHTML转义及模板系统
image常见图形格式的访问及生成
io实现I/O原始访问接口及访问封装
math数学库
net网络库,支持Socket、HTTP、邮件、RPC、SMTP等
os操作系统平台不依赖平台操作的封装
path兼容各操作系统的路径操作实用函数
pluginGo1.7加入的插件系统,支持将代码编译为插件,按需加载
reflect语言反射支持,可以动态获得代码中的类型信息,获取和修改变量的值
regexp正则表达式封装
runtime运行时接口
sort排序接口
strings字符串转换、解析及实用函数
time时间接口
text文本模板及Token语法器
Go

(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的烫烫烫和屯屯屯



(1)标准初始化形式

var 变量名 类型 = 表达式
var hp int = 100



(2)编译器推导类型

var attack = 40
var defence = 20
var damageRate float32 = 0.17  // 默认不指出类型,使用float64精度
var damage = float32(attack - defence) * damageRate



(3)短变量声明并初始化

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),与并发息息相关



整型

int8uint8int16uint16int32uint32int64uint64

uint8就是byte类型,int16是C++中的short,int64是C++中的long

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



浮点型

float323.4e38math.MaxFloat32
float641.8e308math.MaxFloat64



布尔型

只有true和false两个值

Go语言不允许将整数强制转换为布尔型

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



字符串

字符串在Go语言中以原生数据类型出现

C++和C#等语言中,字符串以类的方式进行封装

str := "hello world"
ch := "中文"
UTF-8
UTF-8runeUTF-8
C++ASCII

多行字符串

所有的转义字符均无效

const str = `first line
second line
third line
\r\n
`



字符

Go语言中的字符有两种:

uint8byte
runeint32
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 类型转换

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 字符串应用



计算字符串长度

len()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个字节



遍历字符串

获取每一个字符



(1)遍历每一个ASCII字符

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

由于没有使用Unicode,汉字被显示为乱码。



(2)按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, "死神"):])