开始读 Go 源码了
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了开始读 Go 源码了相关的知识,希望对你有一定的参考价值。
参考技术A学完 Go 的基础知识已经有一段时间了,那么接下来应该学什么呢?有几个方向可以考虑,比如说 Web 开发,网络编程等。
在下一阶段的学习之前,写了一个开源项目 Go 开发的一款分布式唯一 ID 生成系统,如果你对这个项目感兴趣的话,可以在 GitHub 上拿到源码。
在写项目的过程中,发现一个问题。实现功能是没问题的,但不知道自己写的代码是不是符合 Go 的风格,是不是够优雅。所以我觉得相比于继续学习应用开发,不如向底层前进,打好基础,打好写 Go 代码的基础。
所以,我决定开始读 Go 标准库源码,Go 一共有 150+ 标准库,想要全部读完的话不是不可能,但绝对是一项大工程,希望自己能坚持下去。
为什么从 Go 标准库的源码开始读呢?因为最近也看了一些 Go 底层原理的书,说实话,像 goroutine 调度,gc 垃圾回收这些内容,根本就看不懂。这要是一上来就读这部分代码,恐怕直接就放弃 Go 语言学习了。
而标准库就不一样了,有一部分代码根本不涉及底层原理,实现也相对简单,同时又能对 Go 的理念加深理解,作为入门再好不过了。然后再由简入深,循序渐进,就像打怪升级一样,一步一步征服 Go。
说了这么多,那到底应该怎么读呢?我想到了一些方法:
可以通过上面的一种或几种方法相结合,然后再不断阅读不断总结,最终找到一个完全适合自己的方法。
下面是我总结的一些标准库及功能介绍:
这里仅仅列举了一部分标准库,更全面的标准库列表大家可以直接看官网。
那么问题来了,这么多库从何下手呢?
我这里做一个简单的分类,由于水平有限,只能做一些简单的梳理,然后大家可以结合自己的实际情况来做选择。
有些库涉及到非常专业的知识,投入产出比可能会比较低。比如 archive 、 compress 以及 crypto ,涉及到压缩算法以及加密算法的知识。
有些库属于工具类,比如 bufio 、 bytes 、 strings 、 path 、 strconv 等,这些库不涉及领域知识,阅读起来比较容易。
有些库属于与操作系统打交道的,比如 os , net 、 sync 等,学习这些库需要对操作系统有明确的认识。
net 下的很多子包与网络协议相关,比如 net/http ,涉及 http 报文的解析,需要对网络协议比较了解。
如果想要深入了解语言的底层原理,则需要阅读 runtime 库。
要想快速入门,并且了解语言的设计理念,建议阅读 io 以及 fmt 库,阅读后会对接口的设计理解更深。
我已经看了一些源码,虽然过程痛苦,但确实非常有用。前期可能理解起来比较困难,用的时间长一些,但形成固定套路之后,会越来越熟悉,用的时间也会更少,理解也会更深刻。
开源项目:
Go语言ioutil包详解
前言
Go语言 ioutil包中提供了一些常用、方便的IO操作函数,我们在平时的时候中可以直接拿来使用。对于IO读操作来说,比较适用于读小文件,因为相关方法都是一次性将内容读入内存,文件太大内存吃不消;对于其它内容,文章通过示例+分析源码的方式做了介绍,一起来看下吧!
相关知识:Go 语言 bytes.Buffer 源码详解之1 Go 语言 bytes.Buffer 源码详解 2
readAll
readAll 是一个内部方法,从入参 reader 中读取全部数据,然后返回读取到的数据以及产生的 error,主要是调用 butes.Buffer 的 ReadFrom 方法(读取完数据产生的EOF error 在这里不算做 error,因为目的就是读取完数据)。
// io.Reader r : 保存着底层数据,数据从 r 中读取
// capacity: 用于设置保存数据的字节缓冲区的初始容量,但是在读取过程中会自动扩容的
func readAll(r io.Reader, capacity int64) (b []byte, err error)
var buf bytes.Buffer
// 如果字节缓冲区在读取过程中一直扩容,最终超出了系统设置的最大容量,会产生 ErrTooLarge panic,在这里捕获,改为返回一个 error
// 如果是其他类型的 panic,保持 panic
defer func()
e := recover()
if e == nil
return
if panicErr, ok := e.(error); ok && panicErr == bytes.ErrTooLarge
err = panicErr
else
panic(e)
()
// 设置默认容量
if int64(int(capacity)) == capacity
buf.Grow(int(capacity))
// 读取数据
_, err = buf.ReadFrom(r)
return buf.Bytes(), err
ReadAll
ReadAll 方法,我们比较常用的工具类方法,一次性读取文件的所有内容并返回,适用于读取小文件,如果文件太大会占用太多内存。调用 ReadAll 方法成功,会读取 io.Reader r 的所有内容,返回的 err == nil,而不是 err == EOF,因为读取完所有数据了,完成了我们的任务,此时 EOF 不应当是 error。
使用示例
func main()
file, err := os.Open("test.txt")
if err != nil
fmt.Println("open file err")
return
c, err := ioutil.ReadAll(file)
fmt.Println(err)
fmt.Println(string(c))
源码解读
func ReadAll(r io.Reader) ([]byte, error)
// 直接调用内部 readAll 方法,默认容量为 512 字节
return readAll(r, bytes.MinRead)
ReadFile
ReadFile 根据文件名读取文件,返回文件的所有内容以及读取过程中产生的error,与ReadAll类似,读取完文件后,EOF 不算做 error,因为已经完成了任务。
使用示例
func main()
c, err := ioutil.ReadFile("test.txt")
fmt.Println(err)
fmt.Println(string(c))
源码解读
func ReadFile(filename string) ([]byte, error)
f, err := os.Open(filename)
if err != nil
return nil, err
defer f.Close()
// 初始化缓冲区的默认容量,如果我们后续获得了文件大小信息,再更新缓冲区容量
var n int64 = bytes.MinRead
// 获取文件信息
if fi, err := f.Stat(); err == nil
// 为了防止 Size == 0, 多分配了 MinRead 个字节的容量,并且防止在填充完缓冲区后再次的分配
// 如果Stat() 方法返回的Size 信息有误,我们要么会浪费一些空间,要么会根据需要重新分配空间,
// 但是大多数情况下我们能获取到正确的信息
if size := fi.Size() + bytes.MinRead; size > n
n = size
// 调用 readAll 方法读取数据
return readAll(f, n)
WriteFile
WriteFile 方法将数据写入文件,如果文件不存在,会先新建文件;如果已存在,会把之前的数据先清空再写入
使用示例
func main()
var buffer bytes.Buffer
for i := 0; i < 100; i++
buffer.WriteString("this is line " + fmt.Sprintf("%d", i) + "\\n")
if err := ioutil.WriteFile("testFile", buffer.Bytes(), 0644); err != nil
fmt.Println(err)
源码解读
// filename指定了文件名,data是要写入的数据,perm 指定了文件权限(如 0644)
func WriteFile(filename string, data []byte, perm os.FileMode) error
// 调用os.OpenFile 方法创建打开文件
f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
if err != nil
return err
// 写入数据
_, err = f.Write(data)
if err1 := f.Close(); err == nil
err = err1
return err
ReadDir
ReadDir 用于获取文件夹下面的所有文件信息(文件夹+文件),返回的数据是文件名有序的
使用示例
func main()
fileList, err := ioutil.ReadDir("/Users/admin/study")
for _, f := range fileList
fmt.Println(f.Name(), f.IsDir())
fmt.Println(err)
/*
1.log false
composetest true
hello.sh false
student.txt false
wordpress true
<nil>
*/
源码解读
func ReadDir(dirname string) ([]os.FileInfo, error)
f, err := os.Open(dirname)
if err != nil
return nil, err
list, err := f.Readdir(-1)
f.Close()
if err != nil
return nil, err
sort.Slice(list, func(i, j int) bool return list[i].Name() < list[j].Name() )
return list, nil
nopCloser
nopCloser
的作用是将一个 io.Reader 类型包装成为了 io.ReadCloser 类型, 实现了 Close() 方法,但什么也没做,只是返回了 nil。
使用示例
在我们需要将一个 io.Reader 类型包装成 io.ReadCloser 类型时,可以直接调用该方法,比如Go 原生的 http NewRequestWithContext 方法,就直接使用了该方法:
func NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*Request, error)
......
// 传入的 body 是 io.Reader 类型,如果不是 io.ReadCloser 类型,调用 ioutil.NopCloser 转一下
rc, ok := body.(io.ReadCloser)
if !ok && body != nil
rc = ioutil.NopCloser(body)
.......
源码解读
type nopCloser struct
io.Reader
func (nopCloser) Close() error return nil
// NopCloser 传入一个 io.Reader 类型,返回 io.ReadCloser 类型
func NopCloser(r io.Reader) io.ReadCloser
return nopCloserr
Discard
Discard
如名字一样,是一个用于丢弃数据的地方,虽然有时候我们不在意数据内容,但可能存在数据不读出来就无法关闭连接的情况,这时候就可以使用 io.Copy(ioutil.Discard, io.Reader) 将数据写入 Discard。Discard 是 io.Writer 类型,是通过 devNull 定义得来的,devNull 实现了 Write 方法(其实什么都没做,直接返回长度,永远成功)
使用示例
Go 原生 http 包,server.go 就使用了 io.Copy(ioutil.Discard, mb)
func (globalOptionsHandler) ServeHTTP(w ResponseWriter, r *Request)
w.Header().Set("Content-Length", "0")
if r.ContentLength != 0
// Read up to 4KB of OPTIONS body (as mentioned in the
// spec as being reserved for future use), but anything
// over that is considered a waste of server resources
// (or an attack) and we abort and close the connection,
// courtesy of MaxBytesReaders EOF behavior.
mb := MaxBytesReader(w, r.Body, 4<<10)
io.Copy(ioutil.Discard, mb)
源码解读
var Discard io.Writer = devNull(0)
type devNull int
var _ io.ReaderFrom = devNull(0)
// 实现了 Write 方法,什么也不做,直接返回长度
func (devNull) Write(p []byte) (int, error)
return len(p), nil
// 实现了 WriteString 方法,什么也不做,直接返回长度
func (devNull) WriteString(s string) (int, error)
return len(s), nil
// sync.Pool 用来做变量复用的,因为我们根本不在意数据内容,只是为了将数据读出来,
// 为了减少内存分配,提高读取时的字节切片复用程度,使用了 sync.Pool,它就像一个黑洞,丢数据进去就行
var blackHolePool = sync.Pool
New: func() interface
b := make([]byte, 8192)
return &b
,
// 从 blackHolePool 中获取字节切片 bufp , 将 io.Reader 的数据读入 bufp,
// 然后再将 bufp 丢入 blackHolePool,如此往复,只使用了一个变量
func (devNull) ReadFrom(r io.Reader) (n int64, err error)
bufp := blackHolePool.Get().(*[]byte)
readSize := 0
for
readSize, err = r.Read(*bufp)
n += int64(readSize)
if err != nil
blackHolePool.Put(bufp)
if err == io.EOF
return n, nil
return
总结
本篇文章我们介绍了 ioutil
包中的相关内容:
- readAll:内部方法,读取所有数据
- ReadAll:外部方法,读取所有数据
- ReadFile:读取文件所有内容
- WriteFile:写入文件内容,如果文件存在会先清空原有内容
- ReadDir:返回文件夹下文件列表
- nopCloser:将io.Reader 类型包装成 io.ReadCloser 类型
- Discard:用于丢弃数据
更多
个人博客: https://lifelmy.github.io/
微信公众号:漫漫Coding路
以上是关于开始读 Go 源码了的主要内容,如果未能解决你的问题,请参考以下文章