第12章 读写数据
12.1 读取用户的输入
- fmt.Scanln扫描来自标准输入的文本,空格分隔,直到换行为止
- fmt.Scanf第一个参数用来格式化字符串
- fmt.Sscan从字符串读取
- bufio.Reader缓冲读,读到指定字符输出一次
|
|
12.2 文件读写
12.2.1 读文件
- go语言中os.File类型的指针叫做文件句柄
|
|
- 将整个文件的内容读到一个字符串里边去 - ioutil.ReadFile(),第一个返回值是[]byte,第二个是error
- 带缓冲的读取 - bufio.Reader.Read()
- 按列读取文件中的数据,空格分隔,fmt.Fscanln(file, str1 str2 str3)
12.2.2 compress包:读取压缩文件
- compress包支持的压缩文件格式为bzip2,flate,gzip,lzw,zlib
|
|
12.2.3 写文件
|
|
- os.O_RDONLY
- os.O_WRONLY
- os.O_CREATE:不存在创建
- os.O_TRUNC:存在截断长度为0,类似清空
- 使用os.Stdout.WriteString(“string”) 可以输出到屏幕
12.3 文件拷贝
|
|
12.4 从命令行读取参数
12.4.1 os包
- os包中的os.Args来处理命令行参数,在程序启动后读取命令行输入的参数
|
|
12.4.2 flag包
- flag包可以用来替换变量
- flag.Parse()扫描参数列表或者常量列表
- flag.Arg(0) 第一个参数
12.5 buffer读取文件
|
|
12.6 用切片读写文件
|
|
12.7 defer关闭文件
函数退出前会执行文件关闭
|
|
12.8 fmt.Fprintf
接口概念阐述例子:
|
|
函数签名:
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error)
第一个参数必须实现io.Writer接口
12.9 json数据格式
编码格式JSON,XML,gob等
术语
- 数据结构->指定格式=序列化或者编码-传输之前
- 指定格式->数据结构=反序列化或者解码-传输之后
序列化是将内存中的数据结构转换成指定格式
json.Marshal() - 编码,web中使用json.MarshalforHTML()
JSON和go类型对应:
- bool-boolean
- float64-number
- string-string
- nil-null
编码要求:
- JSON 对象只支持字符串类型的 key;要编码一个 Go map 类型,map 必须是 map[string]T(T是 json 包中支持的任何类型)
- Channel,复杂类型和函数类型不能被编码
- 不支持循环数据结构;它将引起序列化进入一个无限循环
- 指针可以被编码,实际上是对指针指向的值进行编码(或者指针是 nil)
反序列化
- json.Unmarshal(json, &v)
编码和解码流
|
|
- json到文件 encode
- 文件到json decode
12.10 XML数据格式
- encoding/xml包含了xml解析器来解析xml内容
- marshal(),unMarshal()完成数据结构和xml内容的转换
- xml中的标签
- StartElement
- Chardata
- EndElement
- Comment
- Directive
- ProcInst
|
|
Gob传输数据
- Gob是go自己的以二进制形式序列化和反序列化程序数据的格式
- Gob通常用于远程调用参数和结果的传输,机器和程序之间的数据传输
- Gob只用于go语言中
- Gob 文件或流是完全自描述的:里面包含的所有类型都有一个对应的描述,并且总是可以用 Go 解码,而不需要了解文件的内容。
- 可导出字段会被编码,零值会被忽略
- Gob 使用通用的 io.Writer 接口,通过 NewEncoder() 函数创建 Encoder 对象并调用 Encode();相反的过程使用通用的 io.Reader 接口,通过 NewDecoder() 函数创建 Decoder 对象并调用 Decode()
|
|
- 编码到文件
|
|
12.12 go中的密码学
- hash 包:实现了 adler32、crc32、crc64 和 fnv 校验;
- crypto 包:实现了其它的 hash 算法,比如 md4、md5、sha1 等。以及完整地实现了 aes、blowfish、rc4、rsa、xtea 等加密算法。
- sha1
|
|
第13章 错误处理与测试
- go没有try-catch-finally机制
- go有defer-panic-and-recover机制
- 永远不要忽略错误,否则可能会导致程序崩溃!!
- 普通错误通过返回值来返回
- 真正的异常用panic和recover来处理
- 产生错误的函数会返回两个变量,一个值和一个错误码;如果后者是 nil 就是成功,非 nil 就是发生了错误。
- 为了防止发生错误时正在执行的函数(如果有必要的话甚至会是整个程序)被中止,在调用函数后必须检查错误。
13.1 错误处理
- go中的error接口
|
|
13.1.1 定义错误
- 新建:
err := errors.New("error string")
- 自定义error
|
|
- 错误类型以
Error
结尾,错误变量以err
或者Err
开头
13.1.2 用fmt创建错误对象
|
|
13.2 运行时异常和panic
- 运行时异常:数组越界,断言失败等会触发panic
|
|
- Go panicking:在多层嵌套的函数调用中调用 panic,可以马上中止当前函数的执行,所有的 defer 语句都会保证执行并把控制权交还给接收到 panic 的函数调用者。这样向上冒泡直到最顶层,并执行(每层的) defer,在栈顶处程序崩溃,并在命令行中用传给 panic 的值报告错误情况:这个终止过程就是 panicking
13.3 recover
- recover用于从panic中恢复,停止终止过程,恢复正常执行
- recover中能在defer修饰的函数中使用,用来取得panic调用中传递过来的错误值,如果是正常执行,调用recover会返回nil,不会生效
- panic 会导致栈被展开直到 defer 修饰的 recover() 被调用或者程序中止。
|
|
- recover和catch很像
- defer-panic-recover 在某种意义上也是一种像 if,for 这样的控制流机制
13.4 自定义包中的错误处理和panicking
- 最佳实践
- 在包的内部,总是应该从panic中recover,不允许显式的超出包范围的panic()
- 向包的调用者返回错误值,而不是panic
|
|
|
|
13.5 一种用闭包处理错误的模式
- 每当函数返回时,我们检查是否有错误发生,会很繁琐
- defer/panic/recover和闭包可以优雅的解决这个问题,但是要求所有函数都是同一种签名才可以
|
|
13.6 启动外部命令和程序
- os包又一个StartProcess函数可以调用或者启动外部系统命令和二进制可执行文件
|
|
13.7 go中的单元测试和基准测试
- testing包是用来进行自动化测试,日志和错误报告的
- 单元测试文件名为
*_test.go
,测试文件不会被普通的go编译器编译,不会被部署,只会被gotest编译 func TestAbcde(t *testing.T)
- 测试失败
Fail
失败继续执行FailNow
失败终止执行,继续执行下一个文件Log
打印到错误日志Fatal
Log+FailNow
- 基准测试函数
BenchmarkReverse()
用go test -test.bench=.*
来执行基准函数
13.8 测试实例
|
|
- 测试用例需要覆盖
- 正常的用例
- 反面的用例
- 边界检查用例
13.9 用测试数据表驱动测试
- 将测试的输入数据和期望的结果写在一起组成一个数据表,数据表中的每条记录都是一个含有输入和期望值的完整测试用例
|
|
13.10 性能调试:分析并优化go程序
13.10.1 时间和内存消耗
/usr/bin/time -f '%Uu %Ss %er %MkB %C' goprogexec
用户时间,系统时间,实际时间,最大内存占用
13.10.2 go test调试
- go test -x -v -cpuprofile=prof.out -file x_test.go
13.10.3 pprof调试
- pprof运行时报告数据
- 有用的命令
- topN - 分析结果中最开头的N份样本
- web - 调用统计,浏览器打开
- list - 代码行数和执行消耗时间
第14章 协程和通道
不需要共享内存来通信,而是通过通信来共享内存。
14.1 并发,并行和协程
- 不要使用全局变量或者共享内存,会给你代码在并发运算时带来风险
- go中使用goroutine进行并发运算
- 协程和操作系统的线程不是一对一的关系,协程是根据一个或多个线程的可用性,映射(多路复用,执行于)在他们之上的;协程调度器在 Go 运行时很好的完成了这个工作。
- 协程工作在相同的地址空间中,所以共享内存的方式一定是同步的
- go使用channel来同步协程
- 当系统调用阻塞协程时,其他协程会继续在其他线程上工作
- 协程比线程更轻量级,4K的内存就可以创建一个协程
- 协程通过关键字go来创建
- 协程的栈会根据需要进行伸缩,不会溢出,结束会自动退出清理
- main可以看做一个协程
- init函数中可以运行协程
- 并发程序可能并行,也可能不是
- 并行是一种通过多处理器来提高速度的能力
go中设置GOMASXPROCS来支持多个系统线程的应用,n个核设置GOMAXPROCS=n-1
Go 协程意味着并行(或者可以以并行的方式部署),协程一般来说不是这样的
- Go 协程通过通道来通信;协程通过让出和恢复操作来通信
14.2 协程间的通信
14.2.1 概念
- 通道(channel),就像一个可以用于发送类型化数据的管道,由其负责协程之间的通信,从而避开所有由共享内存导致的陷阱;这种通过通道进行通信的方式保证了同步性。数据在通道中进行传递:在任何给定时间,一个数据被设计为只有一个协程可以对其访问,所以不会发生数据竞争。 数据的所有权(可以读写数据的能力)也因此被传递。
- channel未初始化未nil
- channel只能传递一种类型的数据
- channel实际上是类型化消息的队列,使数据得以传输。它是先进先出(FIFO)的结构所以可以保证发送给他们的元素的顺序
- channel是引用类型,make来初始化
ch1 := make(chan string)
14.2.2 通信操作符<-
- 数据是按照箭头的方向流动的
ch <-int1
int2 := <- ch
<-ch
- channel的收发都是原子操作
|
|
- 协程的同步:
- main() 等待了 1 秒让两个协程完成,如果不这样,sendData() 就没有机会输出。
- getData() 使用了无限循环:它随着 sendData() 的发送完成和 ch 变空也结束了。
- 如果我们移除一个或所有 go 关键字,程序无法运行,Go 运行时会抛出 panic:
14.2.3 通道阻塞
- 通道默认是同步且无缓冲的:接收者接收数据之前,发送不会结束。通道的发送和接收操作都是在对方准备好之前是阻塞的
|
|
14.2.4 通过channel进行协程同步
- 通过channel,两个协程在通信中的某刻同步交换数据
|
|
14.2.5 同步通道-使用带缓冲的通道
ch1 := make(chan string, buf)
buf为个数- 在缓冲满载之前,给通道发送数据是不会被阻塞的
- 在缓冲空之前,读取是不会阻塞的
14.2.6 协程中用通道输出结果
用通道来完成阻塞
|
|
14.2.7 信号量模式
- 协程通过在通道中放置一个值来处理结束的信号
|
|
14.2.8 实现并行的for循环
|
|
14.2.8 用带缓冲通道实现一个信号量
- 信号量是实现互斥锁常见的同步机制
- 带缓冲通道的容量和要同步的资源容量相同
- 通道的长度(当前存放的元素个数)与当前资源被使用的数量相同
- 容量减去通道的长度就是未处理的资源个数(标准信号量的整数值)
|
|
14.2.10 给通道使用for循环
- for-range
|
|
- 通道迭代模式
|
|
14.2.11 通道的方向
- 只接收:
var recv_noly <-chan int
- 只发送:
var send_only chan<- int
14.3 协程的同步:关闭通道-测试阻塞的通道
- 通道可以被显示的关闭
- 发送者需要关闭通道,接收者不需要
- 关闭方法:defer close(ch)
|
|
- for-range来读取通道会检测通道是否会关闭
14.4 使用select切换协程
- select 被称为通信开关
- select可以处理多个通信中的一个
|
|
14.5 通道、超时和计时器
- time.Ticker结构体
|
|
- 可以返回一个通道并且不关闭
|
|
实现了按照指定频率处理请求
- Timer和Ticker类似,但是只会发送一次时间,可以应用在超时模式中
|
|
14.6 协程和恢复
|
|
一个协程发生panic会继续执行其他的协程
14.7 新旧模型对比
旧模式 - 使用共享内存来进行同步
- 首先定义一个结构体处理一个任务
- 各个任务组成任务池来共享内存,引入锁机制
- 任务数量巨大,则锁机制的开销会导致效率急剧下降
新模式 - 使用通道
- 使用一个通道接受需要处理的任务,一个通道接受处理完成的任务及其结果。worker在协程中启动,数量N可以调整
- 不使用锁的机制,因为从通道获取到新任务不存在竞争关系
- 我理解实际上锁机制是实现在了channel中,一个channel的获取是原子性的,从而提高性能
|
|
- 使用锁的情景:
- 访问共享数据结构中的缓存信息
- 保存应用程序上下文和状态信息数据
- 使用通道的情景
- 与异步操作的结果进行交互
- 分发任务
- 传递数据所有权
14.8 惰性生成器的实现
- 生成器是指当被调用时返回一个序列中下一个值的函数,也被称为惰性求值
- int channel实现的生成器
|
|
- 使用空接口,闭包和高阶函数可以实现一个通用的惰性生产期的工厂函数
|
|
14.9 实现Future模式
- Future指的是你使用某一个值之前需要先对其进行计算,因此可以提前计算好
- 求两个矩阵的乘积的逆 -> 先逆运算再乘
|
|
14.10 复用
CS模式
- go中服务器通常会在协程中执行向客户端的响应,故而会对每一个客户端请求启动一个协程,常见操作为客户端请求包含一个通道,服务器向这个通道中发送响应
- 使用协程可以大大提高QPS
|
|
通过信号通道关闭服务器
|
|
14.11 限制同时处理的请求数 - 带缓冲的通道
|
|
14.12 链式协程
- 海量协程的创建和运行
|
|
14.13 在多核心上并行运算
- 信号量模式
|
|
14.14并行化大量数据的计算
- 串行处理流水线
|
|
- 并行计算
|
|
14.15 漏桶算法
- 客户端
|
|
- 服务端
|
|
14.16 对go协程进行基准测试
|
|
14.17 使用通道并发访问对象
|
|
第15章 网络,模板和网页应用
15.1 tcp服务器
- 服务器
|
|
- 客户端
|
|
- 改进后的tcp_server
|
|
15.2 简单的网页服务器
- net/http包
- http.ListenAndServer
- http.URL
- http.Request
- request.FormValue
- request.ParseForm
- http.HandlerFunc 具体函数 - 对应路径,类似于controller
|
|
- http.ListenAndServerTLS - https
15.3 访问并读取页面
http.Head()
来请求URLhttp.Get()
获取Body中的网页内容http.Redirect(w RespoonseWriter, r *Request, url string, code int)
浏览器重定向URLhttp.NotFound(w ResponseWriter, r *Request)
404http.Error(w ResponseWriter, r *Request)
错误信息和错误码http.Request
req.Method
go定义的HTTP状态码
- http.StatusContinue = 100
- http.StatusOK = 200
- http.StatusFound = 302
- http.StatusBadRequest = 400
- http.StatusUnauthorized = 401
- http.StatusForbidden = 403
- http.StatusNotFound = 404
- http.StatusInternalServerError = 500
15.4 网页应用
|
|
15.5 网页应用健壮
- 使用闭包的错误处理模式
|
|
15.6 模板技术动态生成页面
- 模板缓存
|
|
15.7 template包 - 先不看
15.8 多功能网页服务器
15.9 RPC远程调用
net/rpc包实现互相通信
rpc_object
|
|
- server
|
|
- rpc_client
|
|
15.10 基于网络的通道 netchan
- netchan包实现了类型安全的网络通道:允许一个通道两端出现由网络连接的不同计算机
一组导出器会按名称发布一组通道,导入器连接到导出的机器,并按照名称导入这些通道
发送端
|
|
- 接收端
|
|
15.11 websocket
- 服务端
|
|
- 客户端
|
|
15.12 使用smtp发送邮件
- smpt Simple Mail Transfer Protocol
|
|
第16章 常见的错误与陷阱
- 永远不要声明形如
var p*a
的变量 - 永远不要在for循环中改变计数器的变量
- 永远不要在for-range循环中使用一个值去改变自身的值
- 永远不要将goto和前置标签一起使用
- 永远不要忘记在函数名后边加上()
- 永远不要使用new()创建map
- 为一个两类型定义一个String方法时,不要使用fmt.Print或者类似的代码
- 永远不要忘记当终止缓存写入时,使用Flush函数
- 永远不要忽略错误提示,忽略错误会导致程序崩溃
- 不要使用全局变量或者共享内存,并发不安全
- println函数仅仅用于调试
- 使用正确的方式初始化一个元素是切片的映射,例如
map[type]slice
- 一直使用逗号,ok或者checked形式作为类型断言
- 使用一个工厂函数创建并初始化自己定义的类型
- 仅当一个结构体的方法想改变结构体时,使用结构体指针作为方法的接受者,否则使用一个结构体类型
16.1 误用短声明导致变量覆盖
|
|
16.2 误用字符串
- 字符串不可变
- 字符串运算效率低下
- 使用字符数组进行运算
|
|
16.3 发生错误时使用defer关闭一个文件
|
|
- defer只在函数返回时才会被执行,在循环的结尾或其他一些有限范围的代码内是不会被执行的
16.4 new和make
- 切片,Map和channel,用make
- 数组,结构体,值类型用new
16.5 不需要将一个指向切片的指针传递给函数
- 切片实际上就是一个指向潜在数组的指针
- 当切片作为参数传递时,切记不要解引用切片
16.6 使用指针指向接口类型
- 接口类型已经是一个指针了
16.7 使用值类型时误用指针
- 值类型的内存时栈上分配,内存分配快速且开销不大
- 指针类型需要在堆上创建,导致额外的内存分配
16.8 误用协程和通道
- 大多数情况下,通过栈传递参数会更有效率
- 当且仅当代码中并发执行非常重要,才使用协程和通道
16.9 闭包和协程的使用
|
|
16.10 糟糕的错误处理
- 创建一个布尔型变量用于测试错误条件是多余的
- 避免错误检测使代码变得混乱,使用闭包来封装你的错误检验
第17章 Go语言模式
17.1 逗号,ok模式
- 函数返回时检查错误
val, err := pack1.Func()
- 检测Map中是否存在一个键值
if value, isPresent = map1[key1]; isPresent {}
- 检测一个接口类型变量varI是否包含了类型T,类型断言
if value, ok := varI.(T); ok {}
- 检测一个通道ch是否关闭
for input := range ch {}
17.2 defer模式
使用场景 | 例子 |
---|---|
关闭一个文件流 | defer f.Close() |
解锁一个被锁定的资源 | defer mu.Unlock() |
关闭一个通道 | defer close(ch) |
从panic恢复 | defer func(){if err := revocer(); err != nil {}} |
停止一个计时器 | defer tick1.Stop() |
释放一个进程p | defer p,Release() |
停止CPU性能分析并立即写入 | defer pprof.StopCPUProfile() |
17.3 可见性模式
17.4 运算符模式和接口
17.4.1 函数作为运算符
- go中没有函数重载,我们不得不给函数起不同的名称,可以将其作为包的私有函数,并暴露单一的 Add() 函数作为公共 API。可以在嵌套的 switch 断言中测试类型,以便在任何支持的参数组合上执行操作
|
|
17.4.2 方法作为运算符
- 根据接收者的不同,可以区分不同的方法
|
|
17.4.3 使用接口
- 不同类型上执行相同的方法时,创建一个通用化的接口以实现多态
|
|
第18章 实用代码块
18.1 字符串
- 如何修改字符串中的一个字符
|
|
如何获取字符串的子串 -
substr := str[n;m]
如何使用for或者for-range遍历一个字符串
|
|
如何获取一个字符串的字节数 - len()
如何获取一个字符串的字符数 - utf8.RuneCountInString(str)
如何连接字符串
with a bytes.Buffer
Strings.Join()
+=
- 如何解析命令行参数:使用os或者flag包
18.2 数组和切片
创建
arr1 := new([len]type)
slice1 := make([]type, len)
初始化
arr1 := [...]type{i1,i2,i3,i4,i5}
arrKeyValue := [len]type{i1:val1, i2: val2}
var slice1 []type = arr1[start:end]
截断数组或者切片的最后一个元素
line = line[:len(line)-1]
遍历数组或者切片
|
|
- 二维数组或者切片中查找指定值V
|
|
18.3 Map
创建
map1 := make(map[keyType]valueType)
初始化
map1 := map[string]int{"one":1, "two":2}
遍历
for key, val := range map1{}
在map中检测key1是否存在
val1, isPresent = map1[key1]
删除一个键
delete(map1, key1)
18.4 结构体
- 创建
|
|
初始化
ms := &struct1{10,10,"chris"}
结构体命名大写开头则包外可见
- 每个结构体需要定义一个构件函数来初始化结构体
18.5 接口
检测一个值v是否实现了接口Stringer
if v, ok := v.(Stringer); ok {}
如何使用接口实现一个类型分类接口
|
|
18.6 函数
- 使用recover恢复panic
|
|
18.7 文件
- 打开并读取
|
|
- 通过切片读写文件
|
|
18.8 goroutine和channel
tips
- 出于性能考虑建议使用带缓存的通道
- 限制一个通道的数据数量并将它们封装成一个数组
遍历一个channel
for v := range ch {}
检测一个通道ch是否关闭
input, open := <-ch; !open { }
通过一个通道让主程序等待直到协程完成
|
|
- 通道的工厂模板
|
|
- 通道迭代器模板
- 如何限制并发请求的处理数量
|
|
- 如何在多核上实现并行计算
|
|
如何终止一个协程
runtime.Goexit()
简单的超时模板
|
|
- 使用输入通道和输出通道代替锁
|
|
18.9 网络和网页应用
- 制作解析并使模板生效
var strTempl = template.Must(template.New("TName").Parse(strTemplateHTML))
18.10 其他
- 在程序出错时终止程序
|
|
18.11 出于性能考虑的最佳实践和建议
- 尽可能的使用
:=
去初始化声明的一个变量 - 尽可能的使用字符代替字符串
- 尽可能使用切片替代数组
- 尽可能使用数组和切片替换映射
- 如果只想获取切片中的某项值,不需要值的索引,尽可能使用for-range去遍历切片
- 当数组元素是稀疏的,使用映射会降低内存消耗
- 初始化Map时指定其容量
- 当定义一个方法时,使用指针类型所谓方法的接受者
- 在代码中使用常量或者标志提取常量的值
- 尽可能在需要分配大量内存时使用缓存
- 使用缓存模板