Go核心36讲最终讲

ASCII编码

American Standard Code for Informaton Interchange
美国信息交换标准代码

  • ASCII编码使用单个字节byte的二进制数来编码一个字符,标准的ASCII编码用以个字节的最高比特(bit)位作为奇偶校验位
  • UTF:UCS Transformation Format的缩写;
  • UCS: Universal Character Set的缩写
  • UTF也可以被翻译为Unicode转换格式,达标的是字符与字节序列之间的转换方式
  • UTF-8 8个比特作为一个编码单元
  • rune 是go的特有的一个基本数据类型,
  • 一个汉字占三个字节
    str := "Go 爱好者"
    for i,c := range str {
    fmt.Printf("%d: %q [% x]\n", i, c, []bye(string(c)))
    }
    i的值是 0、1、2、5、8 因为从爱开始一个占3个字节

String包

strings.Builder类型的值优势有3点

  • 已经存在的内容不可变,但可以拼接更多的内容
  • 减少了内存分配和内容拷贝的次数
  • 可将内容重置,可重用值

  • go中string类型的值不可变,裁剪用切片表达式,拼接操作可以用操作符+实现
  • 一个string的值会再底层与它的所有副本共用同一个字节数组,由于这里的字节数组永远不会改变,所以这样做绝对安全。
  • Builder 用于构建字符串,而后者用于读取字符串
  • 不过,这类值在使用上也是有约束的。它在被真正使用之后就不能再被复制了,否则就会引发 panic。虽然这个约束很严格,但是也可以带来一定的好处。它可以有效地避免一些操作冲突。虽然我们可以通过一些手段(比如传递它的指针值)绕过这个约束,但这是弊大于利的。最好的解决方案就是分别声明、分开使用、互不干涉。
    Reader值可以让我们很方便地读取一个字符串中的内容。它的高效主要体现在它对字符串的读取
    机制上。在读取的过程中,Reader值会保存已读取的字节的计数,也称已读计数。

这个计数代表着下一次读取的起始索引位置,同时也是高效读取的关键所在,我们可以利用这类信
的Len方法和size方法,计算出其中的已读计数的值。有了它,我们就可以更加灵活地进行字符串
读取了。

  • strings包提供了大量的函数
    Count、IndexRune、Map、Replace、SplitN、Trim

bytes包

String包面向Unicode字符和经过UTF-8编码的字符串,而bytes包面对的则是主要是字节和字节切片

  • bytes.Buffer类型的用途主要是作为字节序列的缓冲区
  • 我今天会主要讲bytes包中最有特色的类型Buffer顾名思义,bytesBuffer类型的用途主要是作为字节序列的缓冲区。
    与strings.Builder类型一样,bytesBuffer也是开箱即用的。但不同的是,
    strings.Builder只能拼接和导出字符串,而bytesBuffer不但可以拼接、截断其中的字节序列,以各种形式导出其中的内容还可以顺序地读取其中的子序列。
    可以说,bytesBuffer是集读、写功能于一身的数据类型。当然了,这些也基本上都是作为一个缓冲区应该拥有的功能。

在内部,bytesBuffer类型同样是使用字节切片作为内容容器的。并且,与strings.Reader类型类似,bytes.Buffer有一个int类型的字段,用于代表已读字节的计数,可以简称为已读计数。
不过,这里的已读计数就无法通过bytesBuffer提供的方法计算出来了。
我们先来看下面的代码:

var buffer1 bytes.Buffer
contents :="Simple byte buffer for marshaling data." fmt.Printf("Writing contents8q...\n",contents)
bufferl.WriteString(contents)
fmt.Printf("The length of buffer:*d\n",buffer1.Len()) fmt.Printf("The capacity of buffer:sd\n",buffer1.Cap())

我先声明了一个bytesBuffer类型的变量buffer1,并写入了一个字符串。然后,我想打印出这
个bytes.Buffer类型的值(以下简称Buffer值)的长度和容量。在运行这段代码之后,我们将会看到如下的输出:

Writing contents "Simple byte buffer for marshaling data."... The length of buffer:39 The capacity of buffer: 64

乍一看这没什么问题。长度39和容量64的含义看起来与我们已知的概念是一致的。我向缓冲区中写入了一个长度为39的字符串,所以buffer1的长度就是39。

根据切片的自动扩容策略,64这个数字也是合理的。另外,可以想象,这时的已读计数的值应该是0,这是因为我还没有调用任何用于读取其中内容的方法。

可实际上,与stringsReader类型的Len方法一样buffer1的Len方法返回的也是内容容器中
未被读取部分的长度,而不是其中已存内容的总长度(以下简称内容长度)。示例如下:

p1:=make([]byte,7) n,:=buffer1Read(p1)
fmt.Printf("dbytes were read.(call Read)\n",n)
fmtPrintf("The length of buffer:ssd\n",buffer1.Len()) fmtPrintf("The capacity of buffer:ssd\n",buffer1.Cap())

当我从buffer1中读取一部分内容,并用它们填满长度为7的字节切片pl之后,bufferl的Len方
法返回的结果值也会随即发生变化。如果运行这段代码,我们会发现,这个缓冲区的长度已经变为
了32。

另外,因为我们并没有再向该缓冲区中写入任何内容,所以它的容量会保持不变,仍是64。

总之,在这里,你需要记住的是,Buffer值的长度是未读内容的长度,而不是已存内容的总长度。它与在当前值之上的读操作和写操作都有关系,并会随着这两种操作的进行而改变,它可能会变得更小,也可能会变得更大。
而Buffer值的容量指的是它的内容容器(也就是那个字节切片)的容量,它只与在当前值之上的写操作有关,并会随着内容的写入而不断增长。
再说已读计数。由于strings.Reader还有一个size方法可以给出内容长度的值,所以我们用内容长度减去未读部分的长度,就可以很方便地得到它的已读计数。
然而,bytes.Buffer类型却没有这样一个方法,它只有Cap方法。可是Cap方法提供的是内容容器的容量,也不是内容长度。

并目,这里的内容容器容量在很多时候都与内容长度不相同,因此,没有了现成的计算公式,只要
遇到稍微复杂些的情况,我们就很难估算出Buffer值的已读计数。

一旦理解了已读计数这个概念,并目能够在读写的过程中,实时地获得已读计数和内容长度的值
我们就可以很直观地了解到当前Buffer值各种方法的行为了。不过,很可惜,这两个数字我们都无法直接拿到。

虽然,我们无法直接得到一个Buffer值的已读计数,并目有时候也很难估算它,但是我们绝对不
能就此作罢,而应该通过研读bytesBuffer和文档和源码,去探究已读计数在其中起到的关键作用。
否则,我们想用好bytesBuffer的意愿,恐怕就不会那么容易实现了。
下面的这个问题,如果你认真地阅读了bytesBuffer的源码之后,就可以很好地回答出来。

我们今天的问题是:bytesBuffer类型的值记录的已读计数,在其中起到了怎样的作用?

这道题的典型回答是这样的。
bytes.Buffer中的已读计数的大致功用如下所示。
1读取内容时,相应方法会依据已读计数找到未读部分,并在读取后更新计数。
2写入内容时,如需扩容,相应方法会根据已读计数实现扩容策略。
3截断内容时,相应方法截掉的是已读计数代表索引之后的未读部分。
4读回退时,相应方法需要用已读计数记录回退点。
5重置内容时,相应方法会把已读计数置为0。
6.导出内容时,相应方法只会导出已读计数代表的索引之后的未读部分。
7获取长度时,相应方法会依据已读计数和内容容器的长度,计算未读部分的长度并返回。

问题解析
通过上面的典型回答,我们已经能够体会到已读计数在bytesBuffer类型,及其方法中的重要性了。没错,bytes.Buffer的绝大多数方法都用到了已读计数,而且都是非用不可。

在课取内容的时候,相应方法会先根据已记计数,判新一下内容容器中是否还有未读的内容,如果
有,那么它就会从已读计数代表的索引处开始读取。

在读取完成后,它还会及时地更新已读计数。也就是说,它会记录一下又有多少个字节被读取了。
这里所说的相应方法包括了所有名称以Read开头的方法,以及Next方法和WriteTo方法。

在写入内容的时候,绝大多数的相应方法都会先检查当前的内容容器,是否有足够的容量容纳新的内容。如果没有,那么它们就会对内容容器进行扩容。

在扩容的时候,方法会在必要时,依据已读计数找到未读部分,并把其中的内容拷贝到扩容后内容容器的头部位置。

然后,方法将会把已读计数的值置为0,以表示下一次读取需要从内容容器的第一个字节开始。用
于写入内容的相应方法,包括了所有名称以write开头的方法,以及ReadFrom方法。
用于截断内容的方法Truncate,会让很多对bytesBuffer不太了解的程序开发者迷惑。它会接受一个int类型的参数,这个参数的值代表了:在截断时需要保留头部的多少个字节。

不过,需要注意的是,这里说的头部指的并不是内容容器的头部,而是其中的未读部分的头部。头部的起始索引正是由已读计数的值表示的。因此,在这种情况下,已读计数的值再加上参数值后得到的和,就是内容容器新的总长度。
在bytes.Buffer中,用于读回退的方法有UnreadByte和UnreadRune。这两个方法分别用于
回退一个字节和回退一个Unicode字符。调用它们一般都是为了退回在上一次被读取内容末尾的那个分隔符,或者为重新读取前一个字节或字符做准备。
不过,退回的前提是,在调用它们之前的那一个操作必须是“读取”,并且是成功的读取,否则这些方法就只能忽略后续操作并返回一个非ni1的错误值。
UnreadByte方法的做法比较简单,把已读计数的值减1就好了。而UnreadRune方法需要从已读计数中减去的,是上一次被读取的Unicode字符所占用的字节数。
这个字节数由bytesBuffer的另一个字段负责存储,它在这里的有效取值范围是[14]。只有 ReadRune方法才会把这个字段的值设定在此范围之内。

由此可见,只有紧接在调用ReadRune方法之后,对UnreadRune方法的调用才能够成功完成。该方法明显比UnreadByte方法的适用面更窄。
我在前面说过,bytesBuffer的Len方法返回的是内容容器中未读部分的长度,而不是其中已存内容的总长度(即:内容长度)。
而该类型的Bytes方法和string方法的行为,与Len方法是保持一致的。前两个方法只会去访问未读部分中的内容,并返回相应的结果值。

在我们剖析了所有的相关方法之后,可以这样来总结:在已读计数代表的索引之前的那些内容,永
远都是已经被读过的,它们几乎没有机会再次被读取。
不过,这些已读内容所在的内存空间可能会被存入新的内容。这一般都是由于重置或者扩充内容容
器导致的。这时,已读计数一定会被置为0,从而再次指向内容容器中的第一个字节,这有时候也
是为了避免内存分配和重用内存空间。

os.File

os.File类型都实现了哪些io包中的接口?
这道题的典型回答是这样的。
os.File类型拥有的都是指针方法,所以除了空接口之外,它本身没有实现任何接口。而它的指针类型则实现了很多io代码包中的接口。
首先,对于io包中最核心的3个简单接口io.Reader ioWriter和io.closer,*os.File类型都实现了它们。

其次,该类型还实现了另外的3个简单接口,即:io.ReaderAtio.Seeker和
io.WriterAt。

正是因为*osFile类型实现了这些简单接口,所以它也顺便实现了io包的9个扩展接口中的7
个。然而,由于它并没有实现简单接口ioByteReader和io.RuneReader,所以它没有实现分别作为这两者的扩展接口的io.ByteScanner和io.RuneScanner。

总之,osFile类型及其指针类型的值,不但可以通过各种方式读取和写入某个文件中的内容,还可以寻找并设定下一次读取或写入时的起始索引位置,另外还可以随时对文件进行关闭。
但是,它们并不能专门地读取文件中的下一个字节或者下一个Unicode字符,也不能进行任何的读回退操作。不过,单独读取下一个字节或字符的功能也可以通过其他方式来实现,比如,调用它的 Read方法并传入适当的参数值就可以做到这一点。

问题解析
这个问题其实在间接地问osFile类型能够以何种方式操作文件?"我在前面的典型回答中也给出了简要的答案。在我进一步地说明一些细节之前,我们先来看看怎样才能获得一个osFile类型的指针值(以下简称File值)。
在os包中,有这样几个函数,即:CreateNewFile Open和openFile.

os.Create函数用于根据给定的路径创建一个新的文件。它会返回一个File值和一个错误值。我们可以在该函数返回的File值之上,对相应的文件进行读操作和写操作。
不但如此,我们使用这个函数创建的文件,对于操作系统中的所有用户来说,都是可以读和写的。换句话说,一旦这样的文件被创建出来,任何能够登录其所属的操作系统的用户,都可以在任意时刻读取该文件中的内容,或者向该文件写入内容。
注意,如果在我们给予osCreate函数的路径之上已经存在了一个文件,那么该函数会先清空现有文件中的全部内容,然后再把它作为第一个结果值返回。
另外,osCreate函数是有可能返回非nil的错误值的。比如,如果我们给定的路径上的某一级父
目录并不存在,那么该函数就会返回一个*osPathError类型的错误值,以表示“不存在的文件或
目录”。
再来看osNewFile函数。该函数在被调用的时候需要接受一个代表文件描述符的、uintptr类型的值,以及一个用于表示文件名的字符串值。
如果我们给定的文件描述符并不是有效的,那么这个函数将会返回nil,否则,它将会返回一个代表了相应文件的File值。

注意,不要被这个函数的名称误导了,它的功能并不是创建一个新的文件,而是依据一个已经存在
的文件的描述符,来新建一个包装了该文件的File值。

例如,我们可以像这样拿到一个包装了标准错误输出的File值:

file3:=osNewFile(uintptr(syscall.Stderr),"/dev/stderr")

然后,通过这个File值向标准错误输出上写入一些内容:

if file3 != nil { defer file3.Close()
file3.WriteString(
"The Go language program writes the contents into stderr.\n")}

os.0pen函数会打开一个文件并返回包装了该文件的File值。然而,该函数只能以只读模式打开
文件。换句话说,我们只能从该函数返回的File值中读取内容,而不能向它写入任何内容。

如果我们调用了这个File值的任何一个写入方法,那么都将会得到一个表示了“坏的文件描述符”的错误值。实际上,我们刚刚说的只读模式,正是应用在File值所持有的文件描述符之上的。

所谓的文件描述符,是由通常很小的非负整数代表的。它一般会由1/0相关的系统调用返回,并作
为某个文件的一个标识存在。
从操作系统的层面看,针对任何文件的1/0操作都需要用到这个文件描述符。只不过,Go语言中的一些数据类型,为我们隐匿掉了这个描述符,如此一来我们就无需时刻关注和辨别它了(就像 os.File类型这样)。
实际上,我们在调用前文所述的osCreate函数os0pen函数以及将会提到的osOpenFile函
数的时候,它们都会执行同一个系统调用,并目在成功之后得到这样一个文件描述符。这个文件描
述符将会被储存在它们返回的File值中。
os.File类型有一个指针方法,名叫Fd。它在被调用之后将会返回一个uintptr类型的值。这个值就代表了当前的File值所持有的那个文件描述符。

不过,在os包中,除了NewFile函数需要用到它,它也没有什么别的用武之地了。所以,如果你操作的只是常规的文件或者目录,那么就无需特别地在意它了。
最后,再说一下os.0penFile函数。这个函数其实是osCreate函数和os0pen函数的底层支持,它最为灵活。
这个函数有3个参数,分别名为name、flag和perm。其中的name指代的就是文件的路径。而
flag参数指的则是需要施加在文件描述符之上的模式,我在前面提到的只读模式就是这里的一个可
选项。
在Go语言中,这个只读模式由常量oS0RDONLY代表,它是int类型的。当然了,这里除了只读模式之外,还有几个别的模式可选,我们稍后再细说。

os.0penFile函数的参数perm代表的也是模式,它的类型是osFileMode,此类型是一个基干
uint32类型的再定义类型。
为了加以区别,我们把参数flag指代的模式叫做操作模式,而把参数perm指代的模式叫做权限模式。可以这么说,前者限定了操作文件的方式,而后者则可以控制文件的访问权限。关于权限模式的更多细节我们将在后面讨论。

到这里,你需要记住的是,通过osFile类型的值,我们不但可以对文件进行读取,写入、关闭等
操作,还可以设定下一次读取或写入时的起始索引位置。
此外,os包中还有用于创建全新文件的Create函数,用于包装现存文件的NewFile函数,以及可被用来打开已存在的文件的Open函数和OpenFile函数。

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注