go实战

以下是阅读英文版《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创建方式

  1. make
  2. 切片字面量( 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")

图片

桶内部实现

映射使用两个数据结构来存储数据。

  1. 数组:内部存储的是用于选择桶的散列键的高八位值。这个数组用于区分每个 键值对要存在哪个桶里;
  2. 字节数组:用于存储键值对。该字节数组先依次 存储了这个桶里所有的键,之后依次存储了这个桶里所有的值。实现这种键值对的存储方式目的 在于减少每个桶所需的内存。

图片

所以,map的底层任然是基于数组实现的。

golang中map的内部实现

需要查资料,补齐

map定义和初始化

  1. make
dict := make(map[string]int)
  1. 通过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种类型:

  1. 值类型
  2. 指针类型
    |实际接受者类型|定义方法接受者类型|编译器行为|
    |:----|:----|:----|
    |值类型|值类型| |
    |值类型|指针类型|先对值类型取地址 &|
    |指针类型|指针类型| |
    |指针类型|值类型|先对指针类型解引用|

如果使用值接收者声明方法,调用时会使 用这个值的一个副本来执行

Interfaces

首先接口是一种类型,用于定义行为的类型。这些被定义的行为不由接口直接实现,而是通过用户定义的类型方法实现。用户定义的类型叫做实体类型

不同实体类型赋值给接口变量后再内存中的表现

  1. 实体类型是值类型

图片

接口值是一个两个字长度 的数据结构,第一个字包含一个指向内部表的指针。这个内部表叫作 iTable,包含了所存储的 值的类型信息。iTable 包含了已存储的值的类型信息以及与这个值相关联的一组方法。第二个 字是一个指向所存储值的指针

  1. 实体类型是指针类型

图片

类型信息会存储一个指 向保存的类型的指针,而接口值第二个字依旧保存指向实体值的指针

方法集

方法集定义了一组关联到给定类型的值或者指针的方法

图片

为什么指针接收者只能传递对应类型的指针?因为编译器并不总时能获取值的地址。e.g.

图片

多态

多态结构:定义一个接口,定义多个用户类型并实现接口中的方法,最后定义一个多态函数

嵌入类型

2个概念

  1. 内部类型
  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)
}

总结

  1. 使用关键字 struct 或者通过指定已经存在的类型,可以声明用户定义的类型
  2. 方法提供了一种给用户定义的类型增加行为的方式
  3. 设计类型时需要确认类型的本质是原始的,还是非原始的???
  4. 接口是声明了一组行为并支持多态的类型
  5. 嵌入类型提供了扩展类型的能力,而无需使用继承
  6. 标识符要么是从包里公开的,要么是在包里未公开的

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)
比较并交换操作

同步访问共享资源的方式

  1. 原子操作
  2. 加锁
  3. 通道

通道

可以通过通道共享内置类型命名类型结构类型引用类型的值或者指针

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值