Chapter 1
Go语言特点:内置并发机制,类型系统简单高效,自带垃圾回收器
Go语言是一种编译型语言,编译器只会关注那些直接被引用的库,而不是像C,JAVA遍历依赖链中所有依赖的库
内置并发机制
goroutine
goroutine类似线程,但是其占用的内存远小于线程,使用需要的代码更少。
net\http
库直接使用了内置的 goroutine
,并且每个收到的请求都自动在其自己的 goroutine
里处理
Go语言运行时会自动在配置的一组逻辑处理器上调度执行 goroutine
。每个逻辑处理器绑定到一个操作系统线程上。
通道
通道是一种内置的数据结构,可以让用户在不同的 goroutine
之间同步发送具有类型的消息。发挥着锁机制的作用,保证了并行运行时数据的一致性。
通道保证了同一时刻只有一个 goroutine
修改数据。通道用于在几个运行的 goroutine
之间发送数据
Go语言的类型系统
使用 组合 设计模式,只需简单的将一个类型嵌入到另一个类型,就能复用所有功能,避免了传统的基于继承的模型。还具有独特的接口实现机制,允许用户对行为进行建模而不是对类型进行建模
Go支持用户定义的类型,通常包含一组带类型的字段,用于存储数据,可以声明操作该类型数据的方法
接口 用于描述类型的行为(如果一个类型的实例实现了一个接口,意味着这个实例可以执行一组特定的行为)
如果一个类型实现了一个接口的所有方法,那么这个类型的实例就可以存储在这个接口类型的实例中,不需要额外声明
内存管理
Go语言有垃圾回收机制, 不需要事先分配内存,事后还需释放
Chapter 2
一个典型的 Go
语言的代码形式为:
-sample
-data
data.json
-matchers
rss.go
-search
default.go
..等相关文件
main.go
可以看到 ,所有文件都包含在 sample
文件夹中,并且main.go
为整个程序的入口,负责调用各个子目录中的代码文件(树状结构)
main包
每个可执行的 go
程序首先要声明一个 main
函数,将其作为程序的入口,另外还需要包含 main
包(package main)
package main
//package main 说明这是一个名为main的包。一个包定义一组编译过的代码,包的名字类似命名空间,可以用来间接访问包内声明的标识符
import ( //import用处是导入另外的包(感觉类似 include)
"log"
"os"
_ "github.com/goinaction/code/chapter2/sample/matchers"
"github.com/goinaction/code/chapter2/sample/search"
// “_” 让Go语言对包做初始化操作,但是并不使用包里的标识符
)
// 代码中每个init函数都会在main函数执行前调用
func init() {
// 将标准库里的日志输出设置为标准输出(stdout)设备
log.SetOutput(os.Stdout)
}
func main() {
// 调用search包里的run函数用来搜索
search.Run("president")
}
search包
search.go
Go语言中所有变量都被初始化为其类型的零值
简化变量声明运算符 := 用于声明一个变量,同时赋值,但是不需要声明变量类型,编译器根据函数返回值类型自己确定
如果需要声明初始值为零值的变量,需要使用 var
关键字声明变量,如果提供缺爹的非零值初始化变量或者使用函数返回值创建变量,应该使用 :=
在 main
函数返回前需要清理并终止所有之前启动的 goroutine
(因为 main
函数返回,整个程序也就终止)
for range
for range
可以用于迭代数组、字符串、切片、映射和通道,并且每次返回两个值(迭代的元素在切片里的索引位置,元素值的一个副本)
“_”
-
让Go语言对包做初始化操作,但是并不使用包里的标识符
-
占位符,占据保存了返回值的变量的位置。用于要调用的函数返回多个值但是不需要每个值的情况,就可以使用下划线将其忽略
map
查找map里的键时,要么将结果赋值给一个变量(查找的结果值),要么赋值给两个变量(map查找的结果值,布尔标志(说明查找的键是否存在))
匿名函数
func(接受的参数){代码段}(传入的参数)
package search
import (
"log"
"sync" //提供goroutine同步作用
)
/*
注册用于搜索的匹配器的映射
没有定义在任何函数内,所以当成包级变量(全局变量)使用。标识符要么从包里公开,要么不公开。所以以小写字母开头的变量名不公开,以大写字母开头的公开
map是一个引用类型,需要make来构造。
*/
var matchers = make(map[string]Matcher)
// Run performs the search logic.
func Run(searchTerm string) {
// 获取需要搜索的数据源列表,调用了search包中的RetrieveFeeds函数,有两个返回值(Feed类型的切片,错误值)
feeds, err := RetrieveFeeds()
if err != nil { //检查是否为真的错误,为真进入if语句
log.Fatal(err)
}
// 创建一个无缓冲的通道,接受匹配后的结果
results := make(chan *Result)
// 构建一个waitgroup,以便处理所有的数据源,防止程序在全部搜索执行完之前终止
// waitgroup是一个技术信号量,可以利用它来统计所有的goroutine是否都完成了工作
var waitGroup sync.WaitGroup
// 设置需要等待处理每个数据源的goroutine的数量
waitGroup.Add(len(feeds))
// 为每个数据源启动一个goroutine来查找结果
for _, feed := range feeds {
// 获取一个匹配器用于查找
matcher, exists := matchers[feed.Type]
if !exists {
matcher = matchers["default"]
}
// 启动一个goroutine来执行搜索
go func(matcher Matcher, feed *Feed) {
Match(matcher, feed, searchTerm, results)
waitGroup.Done()
}(matcher, feed)
}
// 启动一个goroutine来监控是否完成所有工作
go func() {
// 等候
waitGroup.Wait()
close(results)
}()
// 显示返回结果
Display(results)
}
// Register is called to register a matcher for use by the program.
func Register(feedType string, matcher Matcher) {
if _, exists := matchers[feedType]; exists {
log.Fatalln(feedType, "Matcher already registered")
}
log.Println("Register", feedType, "matcher")
matchers[feedType] = matcher
}
数组、字符串和切片
数组
数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。数组的长度是数组类型的组成部分。
数组定义 :
// 第一种方式是定义一个数组变量的最基本的方式,数组的长度明确指定,数组中的每个元素都以零值初始化。 var a [3]int // 定义长度为3的int型数组, 元素全部为0 // 第二种方式定义数组,可以在定义的时候顺序指定全部元素的初始化值,数组的长度根据初始化元素的数目自动计算。 var b = [...]int{1, 2, 3} // 定义长度为3的int型数组, 元素为 1, 2, 3 // 第三种方式是以索引的方式来初始化数组的元素,因此元素的初始化值出现顺序比较随意。这种初始化方式和map[int]Type类型的初始化语法类似。数组的长度以出现的最大的索引为准,没有明确初始化的元素依然用0值初始化。 var c = [...]int{2: 3, 1: 2} // 定义长度为3的int型数组, 元素为 0, 2, 3 // 第四种方式是混合了第二种和第三种的初始化方式,前面两个元素采用顺序初始化,第三第四个元素零值初始化,第五个元素通过索引初始化,最后一个元素跟在前面的第五个元素之后采用顺序初始化。 var d = [...]int{1, 2, 4: 5, 6} // 定义长度为6的int型数组, 元素为 1, 2, 0, 0, 5, 6
Go语言中数组是值语义。一个数组变量即表示整个数组,它并不是隐式的指向第一个元素的指针(比如C语言的数组),而是一个完整的值。当一个数组变量被赋值或者被传递的时候,实际上会复制整个数组。如果数组较大的话,数组的赋值也会有较大的开销。为了避免复制数组带来的开销,可以传递一个指向数组的指针,但是数组指针并不是数组。
数组遍历:
var a = [...]int{1, 2, 3} // a 是一个数组
var b = &a // b 是指向数组的指针
for i := range a {
fmt.Printf("a[%d]: %d\n", i, a[i])
}
for i, v := range b {
fmt.Printf("b[%d]: %d\n", i, v)
}
for i := 0; i < len(c); i++ {
fmt.Printf("c[%d]: %d\n", i, c[i])
}
字符串
一个字符串是一个不可改变的字节序列,字符串通常是用来包含人类可读的文本数据。和数组不同的是,字符串的元素不可修改,是一个只读的字节数组。每个字符串的长度虽然也是固定的,但是字符串的长度并不是字符串类型的一部分。
字符串虽然不是切片,但是支持切片操作,不同位置的切片底层也访问的同一块内存数据(因为字符串是只读的,相同的字符串面值常量通常是对应同一个字符串常量)
切片
切片就是一种简化版的动态数组。因为动态数组的长度是不固定,切片的长度自然也就不能是类型的组成部分了。
切片的结构定义:
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
切片多了一个Cap
成员表示切片指向的内存空间的最大容量(对应元素的个数,而不是字节数)
切片定义:
var (
a []int // nil切片, 和 nil 相等, 一般用来表示一个不存在的切片
b = []int{} // 空切片, 和 nil 不相等, 一般用来表示一个空的集合
c = []int{1, 2, 3} // 有3个元素的切片, len和cap都为3
d = c[:2] // 有2个元素的切片, len为2, cap为3
e = c[0:2:cap(c)] // 有2个元素的切片, len为2, cap为3
f = c[:0] // 有0个元素的切片, len为0, cap为3
g = make([]int, 3) // 有3个元素的切片, len和cap都为3
h = make([]int, 2, 3) // 有2个元素的切片, len为2, cap为3
i = make([]int, 0, 3) // 有0个元素的切片, len为0, cap为3
)
内置的len
函数返回切片中有效元素的长度,内置的cap
函数返回切片容量大小,容量必须大于或等于切片的长度。
添加
主要使用 append
操作
在尾部追加:
var a []int
a = append(a, 1) // 追加1个元素
a = append(a, 1, 2, 3) // 追加多个元素, 手写解包方式
a = append(a, []int{1,2,3}...) // 追加一个切片, 切片需要解包
在头部追加:
var a = []int{1,2,3}
a = append([]int{0}, a...) // 在开头添加1个元素
a = append([]int{-3,-2,-1}, a...) // 在开头添加1个切片
在容量不足的情况下,append
的操作会导致重新分配内存,可能导致巨大的内存分配和复制数据代价。即使容量足够,依然需要用append
函数的返回值来更新切片本身,因为新切片的长度已经发生了变化。在开头一般都会导致内存的重新分配,而且会导致已有的元素全部复制1次。因此,从切片的开头添加元素的性能一般要比从尾部追加元素的性能差很多。
在中间追加:
var a []int
a = append(a[:i], append([]int{x}, a[i:]...)...) // 在第i个位置插入x
a = append(a[:i], append([]int{1,2,3}, a[i:]...)...) // 在第i个位置插入切片
这样操作会产生一个中间切片,造成资源浪费
a = append(a, 0) // 切片扩展1个空间
copy(a[i+1:], a[i:]) // a[i:]向后移动1个位置
a[i] = x // 设置新添加的元素
删除
根据要删除元素的位置有三种情况:从开头位置删除,从中间位置删除,从尾部删除。其中删除切片尾部的元素最快:
a = []int{1, 2, 3}
a = a[:len(a)-1] // 删除尾部1个元素
a = a[:len(a)-N] // 删除尾部N个元素
删除开头的元素可以直接移动数据指针:
a = []int{1, 2, 3}
a = a[1:] // 删除开头1个元素
a = a[N:] // 删除开头N个元素
对于删除中间的元素,需要对剩余的元素进行一次整体挪动,同样可以用append
或copy
原地完成
a = []int{1, 2, 3, ...}
a = append(a[:i], a[i+1:]...) // 删除中间1个元素
a = append(a[:i], a[i+N:]...) // 删除中间N个元素
a = a[:i+copy(a[i:], a[i+1:])] // 删除中间1个元素
a = a[:i+copy(a[i:], a[i+N:])] // 删除中间N个元素
CGo
代码通过 import "C"
语句启用 CGO
特性,要紧跟被注释掉的C代码后
Go中包含C
-
// hello.go package main /* #include <stdio.h> static void SayHello(const char* s) { puts(s); } */ import "C" func main() { C.SayHello(C.CString("Hello, World\n")) }
此时直接
cmd
中输入go run hello.go
即可运行 -
// hello.c #include <stdio.h> void SayHello(const char* s) { puts(s); } // hello.go package main //void SayHello(const char* s); import "C" func main() { C.SayHello(C.CString("Hello, World\n")) }
可以将
SayHello
函数放到当前目录下的一个C语言源文件中,运行时采用go run "your/package"
或go build "your/package"
才可以。若就在包路径下,直接运行go build
-
或者也可以生成C的头文件,这样在引用时只需要把
//void SayHello(const char* s);
改成//#include <hello.h>
:// hello.h void SayHello(/*const*/ char* s); // hello.c #include <stdio.h> #include "hello.h" void SayHello(const char* s) { puts(s); } // hello.go package main //#include "hello.h" import "C" func main() { C.SayHello(C.CString("Hello, World\n")) }
综合版本:
package main /*函数声明*/ //void SayHello(char* s); import "C" import ( "fmt" ) func main() { /*调用SayHello C版本*/ C.SayHello(C.CString("Hello, World\n")) } /*将go版本导出为C的版本*/ //export SayHello func SayHello(s *C.char) { fmt.Print(C.GoString(s)) }
先从Go语言的
main
函数,到CGO自动生成的C语言版本SayHello
桥接函数,最后又回到了Go语言环境的SayHello
函数。
Go是强类型语言,所以cgo中传递的参数类型必须与声明的类型完全一致,而且传递前必须用”C”中的转化函数转换成对应的C类型,不能直接传入Go中类型的变量。同时通过虚拟的C包导入的C语言符号并不需要是大写字母开头,它们不受Go语言的导出规则约束。
在import "C"
语句前的注释中可以通过#cgo
语句设置编译阶段和链接阶段的相关参数。编译阶段的参数主要用于定义相关宏和指定头文件检索路径。链接阶段的参数主要是指定库文件检索路径和要链接的库文件。
类型转换
C语言类型 | CGO类型 | Go语言类型 |
---|---|---|
char | C.char | byte |
singed char | C.schar | int8 |
unsigned char | C.uchar | uint8 |
short | C.short | int16 |
unsigned short | C.ushort | uint16 |
int | C.int | int32 |
unsigned int | C.uint | uint32 |
long | C.long | int32 |
unsigned long | C.ulong | uint32 |
long long int | C.longlong | int64 |
unsigned long long int | C.ulonglong | uint64 |
float | C.float | float32 |
double | C.double | float64 |
size_t | C.size_t | uint |
在CGO中,C语言的int
和long
类型都是对应4个字节的内存大小,size_t
类型可以当作Go语言uint
无符号整数类型对待。
CGO中,虽然C语言的int
固定为4字节的大小,但是Go语言自己的int
和uint
却在32位和64位系统下分别对应4个字节和8个字节大小。
C语言的结构体、联合、枚举类型不能作为匿名成员被嵌入到Go语言的结构体中。在Go语言中,我们可以通过C.struct_xxx
来访问C语言中定义的struct xxx
结构体类型。
如果结构体的成员名字中碰巧是Go语言的关键字,可以通过在成员名开头添加下划线来访问
在C语言中,我们无法直接访问Go语言定义的结构体类型。
对于联合类型,我们可以通过C.union_xxx
来访问C语言中定义的union xxx
类型。但是Go语言中并不支持C语言联合类型,它们会被转为对应大小的字节数组。
数组、字符串和切片
// Go string to C string
// The C string is allocated in the C heap using malloc.
// It is the caller's responsibility to arrange for it to be
// freed, such as by calling C.free (be sure to include stdlib.h
// if C.free is needed).
func C.CString(string) *C.char
// Go []byte slice to C array
// The C array is allocated in the C heap using malloc.
// It is the caller's responsibility to arrange for it to be
// freed, such as by calling C.free (be sure to include stdlib.h
// if C.free is needed).
func C.CBytes([]byte) unsafe.Pointer
// C string to Go string
func C.GoString(*C.char) string
// C data with explicit length to Go string
func C.GoStringN(*C.char, C.int) string
// C data with explicit length to Go []byte
func C.GoBytes(unsafe.Pointer, C.int) []byte