以下是阅读英文版《go in action》一书时做的笔记,建议看英文原文《go in action》
Chapter 1 Introducing Go
并发
go中并发通过Goroutine实现,而不是通过thread实现(C/JAVA),一个OS thread上可以运行多个Goroutine
一个逻辑处理器绑定到一个OS thread
内存管理
go语言通过垃圾回收(GC)机制管理内存
类型系统
go语言通过**组合(composition)**实现面向对象
在线IDE
http://play.golang.org是在线IDE,可以编辑、运行、调试、分享代码
Chapter 2 Go quick-start
包
包类似于名称空间,所有处于同一个文件夹里的代码文件,必须使用同一个包名。按照惯例,包和文件夹 同名。
main 函数保存在名为 main 的包里。如果 main 函数不在 main 包里,构建工 具就不会生成可执行的文件。
go语言导入的包必须使用。在导入包前面加入下划线,表示值初始化包(调用包的init函数),而不使用包中定义的标识符。
make
在go语言中任何引用类型(slice,map,channel)在使用之前都需要使用make函数构造,如果不先构造 map 并将构造后 的值赋值给变量,会在试图使用这个 map 变量时收到出错信息。对于引用类型来说, 所引用的底层数据结构会被初始化为对应的零值。但是被声明为其零值的引用类型的变量,会返 回 nil 作为其值
goroutine同步
sync 包的 WaitGroup是一个计数信号量,我们可以利用它来统计所有的 goroutine 是不是都完成了工作
range
关键字 range 可以用于迭代数组、字符串(可以理解为一个字符数组)、切片、映射和通道。range返回的集合元素的副本而不是引用
Chapter 3 Packaging and tooling
main包
Go 语言里,命名为 main 的包具有特殊的含义。Go 语言的编译程序会试图把这种名字的 包编译为二进制可执行文件
环境变量
GOROOT:go安装目录,存储go标准库
GOPATH:由用户自定义,存储第三方库
在进行包导入时候优先去GOROOT中找,找不到再去GOPATH中找,只要找到一个就停止查找
命名导入
如果要导入的多个包具有相同的名字就需要使用命名导入来解决了。e.g.
import (
"fmt"
myfmt "mylib/fmt"
)
init函数
每个包可以包含任意多个 init 函数,这些函数都会在程序执行开始的时候被调用
文档阅读
go语言提供2种文档阅读方式:命令行模式和web模式。最好用的个人认为是命令行模式。首先需要安装go工具集
apt install golang-golang-x-tools
# 本地启动web浏览器
godoc -http=:6060
Chapter 4 Arrays, slices, and maps
数组复制
复制数组指针,只会复制指针的值,而不会复制指针所指向的值
[3]*string
array2 := [3]*string{new(string), new(string), new(string)}
*array2[0] = "Red"
*array2[1] = "Blue"
*array2[2] = "Green"
array1 = array2
slice
切片有 3 个字段 的数据结构分别是指向底层数组的指针、切片访问的元素的个数(即长度)和切片允许增长 到的元素个数(即容量)
slice创建方式
- make
- 切片字面量( slice literal)
nil切片 && 空切片
nil切片:指向底层数组的指针字段为nil,len和cap全为0
和空切片:指向底层数组的指针字段不为nil,len和cap全为0
var slice []int
// Use make to create an empty slice of integers. slice := make([]int, 0)
// Use a slice literal to create an empty slice of integers.
slice := []int{}
append
如果切片的底层数组没有足够的可用容量,append 函数会创建一个新的底层数组,将被引 用的现有的值复制到新数组里,再追加新的值。在切片的容量小于 1000 个元素时,总是 会成倍地增加容量。一旦元素个数超过 1000,容量的增长因子会设为 1.25,也就是会每次增加 25% 的容量。
新slice与底层数组分离
如果在创建切片时设置切片的容量和长度一样,就可以强制让新切片的第一个 append 操作创建新的底层数组,与原有的底层数组分离。新切片与原有的底层数组分离后,可以安全地进行后续修改
// Create a slice of strings.
// Contains a length and capacity of 5 elements.
source := []string{"Apple", "Orange", "Plum", "Banana", "Grape"}
// Slice the third element and restrict the capacity.
// Contains a length and capacity of 1 element.
slice := source[2:3:3]
// Append a new string to the slice.
slice = append(slice, "Kiwi")
桶内部实现
映射使用两个数据结构来存储数据。
- 数组:内部存储的是用于选择桶的散列键的高八位值。这个数组用于区分每个 键值对要存在哪个桶里;
- 字节数组:用于存储键值对。该字节数组先依次 存储了这个桶里所有的键,之后依次存储了这个桶里所有的值。实现这种键值对的存储方式目的 在于减少每个桶所需的内存。
所以,map的底层任然是基于数组实现的。
golang中map的内部实现
需要查资料,补齐
map定义和初始化
- make
dict := make(map[string]int)
- 通过key/value (用映射字面量)
dict := map[string]string{"Red": "#da1337", "Orange": "#e95a22"}
map健值
映射的键可以是任何值。这个值的类型可以是内置的类型,也可以是结构类型,只要这个值 可以使用**==**运算符做比较。
但是,切片、函数以及包含切片的结构体类型这些类型由于具有引用语义, 不能作为映射的键,使用这些类型会造成编译错误
在函数间传递映射
在函数间传递映射并不会制造出该映射的一个副本。实际上,当传递映射给一个函数,并对 这个映射做了修改时,所有对这个映射的引用都会察觉到这个修改
总结
- 数组是构造切片和映射的基石
- 内置函数 make 可以创建切片和映射,并指定原始的长度和容量。也可以直接使用切片 和映射字面量,或者使用字面量作为变量的初始值
- 切片有容量限制,不过可以使用内置的 append 函数扩展容量
- 映射的增长没有容量限制
- 内置函数 len 可以用来获取切片或者映射的长度
- 内置函数 cap 只能用于切片
- 通过组合,可以创建多维数组和多维切片。也可以使用切片或者其他映射作为映射的值。 但是切片不能用作映射的键
- 将切片或者映射传递给函数成本很小,并且不会复制底层的数据结构
Chapter 5 Go’s type system
结构体
结构体的零值
// user defines a user in the program.
type user struct {
name string
email string
ext int
privileged bool
}
// Declare a variable of type user.
var bill user
上面通过var关键字定义的结构体变量bill的零值是将结构体每个字段设置为对应类型的零值
方法
关键字 func 和函数名之间的 参数被称作接收者,将函数与接收者的类型绑在一起。如果一个函数有接收者,这个函数就被称 为方法
方法接收者有2种类型:
- 值类型
- 指针类型
|实际接受者类型|定义方法接受者类型|编译器行为|
|:----|:----|:----|
|值类型|值类型| |
|值类型|指针类型|先对值类型取地址 &|
|指针类型|指针类型| |
|指针类型|值类型|先对指针类型解引用|
如果使用值接收者声明方法,调用时会使 用这个值的一个副本来执行
Interfaces
首先接口是一种类型,用于定义行为的类型。这些被定义的行为不由接口直接实现,而是通过用户定义的类型方法实现。用户定义的类型叫做实体类型
不同实体类型赋值给接口变量后再内存中的表现
- 实体类型是值类型
接口值是一个两个字长度 的数据结构,第一个字包含一个指向内部表的指针。这个内部表叫作 iTable,包含了所存储的 值的类型信息。iTable 包含了已存储的值的类型信息以及与这个值相关联的一组方法。第二个 字是一个指向所存储值的指针
- 实体类型是指针类型
类型信息会存储一个指 向保存的类型的指针,而接口值第二个字依旧保存指向实体值的指针
方法集
方法集定义了一组关联到给定类型的值或者指针的方法
为什么指针接收者只能传递对应类型的指针?因为编译器并不总时能获取值的地址。e.g.
多态
多态结构:定义一个接口,定义多个用户类型并实现接口中的方法,最后定义一个多态函数
嵌入类型
2个概念
- 内部类型
- 外部类型
核心
通过嵌入类型,与内部类型相关的标识符会提升到外部类型上。这些被提升的标识符就像直 接声明在外部类型里的标识符一样,也是外部类型的一部分。(就跟java中的继承差不多)
e.g.
package main
import (
"fmt"
)
type notifier interface {
notify()
}
type user struct {
name string
email string
}
func (u *user) notify() {
fmt.Printf("Sending user email to %s<%s>\n",
u.name,
u.email)
}
type admin struct {
user
level string
}
func main() {
// Create an admin user.
ad := admin{
user: user{
name: "john smith",
email: "john@yahoo.com",
},
level: "super",
}
// Send the admin user a notification.
// The embedded inner type's implementation of the
// interface is "promoted" to the outer type.
sendNotification(&ad)
}
// sendNotification accepts values that implement the notifier
// interface and sends notifications.
func sendNotification(n notifier) {
n.notify()
}
外部类型如何覆盖内部类型中的实现?
如果外部类型实现了接口中定义的方法,内部类型的实现就不会被提升。不过内部类型的值一直存在,因此还可以 通过直接访问内部类型的值,来调用没有被提升的内部类型实现的方法
package main
import (
"fmt"
)
type notifier interface {
notify()
}
type user struct {
name string
email string
}
func (u *user) notify() {
fmt.Printf("Sending user email to %s<%s>\n",
u.name,
u.email)
}
type admin struct {
user
level string
}
// 外部类型重新实现接口中声明的方法
func (a *admin) notify() {
fmt.Printf("Sending admin email to %s<%s>\n",
a.name,
a.email)
}
func main() {
// Create an admin user.
ad := admin{
user: user{
name: "john smith",
email: "john@yahoo.com",
},
level: "super",
}
// Send the admin user a notification.
// The embedded inner type's implementation of the
// interface is NOT "promoted" to the outer type.
sendNotification(&ad)
// We can access the inner type's method directly.
ad.user.notify()
// The inner type's method is NOT promoted.
ad.notify()
}
// sendNotification accepts values that implement the notifier
// interface and sends notifications.
func sendNotification(n notifier) {
n.notify()
}
Exporting and unexporting identifiers
当一个标识符的名字以小写字母开头时,这个标识符就是未公开的,即包外的代码不可见。 如果一个标识符以大写字母开头,这个标识符就是公开的,即被包外的代码可见
下面这段代码中New函数可以返回一个非公开的alertCounter 型变量且main函数可以接收到这个变量,为什么呢?
需要两个理由。第一,公开或者未公开的标识符,不是一个值。第二, 短变量声明操作符,有能力捕获引用的类型,并创建一个未公开的类型的变量。
package counters
type alertCounter int
// New creates and returns values of the unexported
// type alertCounter.
func New(value int) alertCounter {
return alertCounter(value)
}
package main
import (
"fmt"
"github.com/goinaction/code/chapter5/listing68/counters"
)
func main() {
// Create a variable of the unexported type using the exported
// New function from the package counters.
counter := counters.New(10)
fmt.Printf("Counter: %d\n", counter)
}
总结
- 使用关键字 struct 或者通过指定已经存在的类型,可以声明用户定义的类型
- 方法提供了一种给用户定义的类型增加行为的方式
- 设计类型时需要确认类型的本质是原始的,还是非原始的???
- 接口是声明了一组行为并支持多态的类型
- 嵌入类型提供了扩展类型的能力,而无需使用继承
- 标识符要么是从包里公开的,要么是在包里未公开的
Chapter 6 Concurrency
原子操作
代码中的加锁操作因为涉及内核态的上下文切换会比较耗时、代价比较高。针对基本数据类型我们还可以使用原子操作来保证并发安全,因为原子操作是Go语言提供的方法它在用户态就可以完成,因此性能比加锁操作更好。Go语言中原子操作由内置的标准库sync/atomic
提供。
atomic包
方法 | 解释 |
---|---|
func LoadInt32(addr *int32) (val int32) func LoadInt64(addr *int64) (val int64) func LoadUint32(addr *uint32) (val uint32) func LoadUint64(addr *uint64) (val uint64) func LoadUintptr(addr *uintptr) (val uintptr) func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer) | 读取操作 |
func StoreInt32(addr *int32, val int32) func StoreInt64(addr *int64, val int64) func StoreUint32(addr *uint32, val uint32) func StoreUint64(addr *uint64, val uint64) func StoreUintptr(addr *uintptr, val uintptr) func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer) | 写入操作 |
func AddInt32(addr *int32, delta int32) (new int32) func AddInt64(addr *int64, delta int64) (new int64) func AddUint32(addr *uint32, delta uint32) (new uint32) func AddUint64(addr *uint64, delta uint64) (new uint64) func AddUintptr(addr *uintptr, delta uintptr) (new uintptr) | 修改操作 |
func SwapInt32(addr *int32, new int32) (old int32) func SwapInt64(addr *int64, new int64) (old int64) func SwapUint32(addr *uint32, new uint32) (old uint32) func SwapUint64(addr *uint64, new uint64) (old uint64) func SwapUintptr(addr *uintptr, new uintptr) (old uintptr) func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer) | 交换操作 |
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool) func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool) func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool) func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool) func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool) func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool) | 比较并交换操作 |
同步访问共享资源的方式
- 原子操作
- 加锁
- 通道
通道
可以通过通道共享内置类型、命名类型、结构类型和引用类型的值或者指针
chapter 7 Concurrency patterns
chapter 8 Standard library
log包
log.SetPrefix("TRACE: ") // 设置日志前缀
log.SetFlags(log.Ldate | log.Lmicroseconds | log.Llongfile) // 配置日志标志位,标志位一般包括:日期时间戳、该日志具体是由哪个源文件记录的、源文件记录日志所在行等
定制日志记录器----示例代码
package main
import (
"io"
"io/ioutil"
"log"
"os"
)
var (
Trace *log.Logger // Just about anything
Info *log.Logger // Important information
Warning *log.Logger // Be concerned
Error *log.Logger // Critical problem
)
func init() {
file, err := os.OpenFile("errors.txt",
os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
log.Fatalln("Failed to open error log file:", err)
}
Trace = log.New(ioutil.Discard,
"TRACE: ",
log.Ldate|log.Ltime|log.Lshortfile)
Info = log.New(os.Stdout,
"INFO: ",
log.Ldate|log.Ltime|log.Lshortfile)
Warning = log.New(os.Stdout,
"WARNING: ",
log.Ldate|log.Ltime|log.Lshortfile)
Error = log.New(io.MultiWriter(file, os.Stderr),
"ERROR: ",
log.Ldate|log.Ltime|log.Lshortfile)
}
func main() {
Trace.Println("I have something standard to say")
Info.Println("Special Information")
Warning.Println("There is something you need to know about")
Error.Println("Something has failed")
}
json包
解码
NewDecoder 函数以及 Decode — 将json文件解码成期望的结构
var gr gResponse // 定义的结构类型
err := json.NewDecoder(resp.Body).Decode(&gr)
Unmarshal 函数 — 需要处理的 JSON 文档会以 string 的形式存在。在这种情况下,需要将 string 转换 为 byte 切片([]byte),并使用 json 包的 Unmarshal 函数进行反序列化的处理
编码
MarshalIndent/Marshal — 这个函数可以很方便地将Go语言的map类型的值或者结构类型的值转换为易读格式的 JSON文档
io包
所有实现了io.Writer 和 io.Reader这两个接口的类型的值,都可以使用 io 包提供的所有功能,也可以用于其他包里接受这两个接口的函数以及方法。
chapter 9 Testing and benchmarking
测试文件命名规范:Go 语言的测试工具只会认为以_test.go 结尾的文件是测试文件
go test