目录
【unicode字符编码】
【strings包】
strings.Builder
strings.Reader
【bytes包】
【io包】
【bufio包】
【os包】
Go 语言的代码是由 Unicode 字符组成的,Go语言的源码文件必须使用 UTF-8 编码格式存储,如果源码文件中出现了非 UTF-8 编码的字符,那么在构建、安装和运行的时候,go命令就会报告错误“illegal UTF-8 encoding”。
Unicode 编码规范通常使用十六进制表示法来表示 Unicode 代码点的整数值,并使用“U+”作为前缀。比如,英文字母字符“a”的 Unicode 代码点是 U+0061。在 Unicode 编码规范中,一个字符能且只能由与它对应的那个代码点表示。Unicode 编码规范提供了三种编码格式:UTF-8、UTF-16、UTF-32,其中的 UTF 是 UCS Transformation Format 的缩写。而 UCS 又是 Universal Character Set 的缩写,但也可以代表 Unicode Character Set。所以,UTF 也可以被翻译为 Unicode 转换格式,它代表的是字符与字节序列之间的转换方式。
UTF-8 是一种可变宽的编码方案,它会用一个或多个字节的二进制数来表示某个字符,最多使用四个字节。比如,对于一个英文字符,它仅用一个字节的二进制数就可以表示,而对于一个中文字符,它需要使用三个字节才能够表示。不论怎样,一个受支持的字符总是可以由 UTF-8 编码为一个字节序列。
在Go语言中,一个string类型的值在底层是由一系列相对应的 Unicode 代码点的 UTF-8 编码值来表达的。一个string类型的值既可以被拆分为一个包含多个字符的序列(由一个以rune为元素类型的切片来表示),也可以被拆分为一个包含多个字节的序列(由一个以byte为元素类型的切片来表示)。
rune是int32类型的一个别名类型,它的一个值就代表一个Unicode字符,比如,'你'、'好'、'A'、'B'、'C' 都可以代表一个 Unicode 字符。一个rune类型的值由四个字节宽度的空间来存储。
func test1() {
//一个rune类型的值由四个字节宽度的空间来存储
str := "你好 ABC "
fmt.Printf("string: %q\n", str) //"你好 ABC "
//字符串值如果被转换为[]rune类型的值,其中的每一个字符(不论是英文字符还是中文字符)就都会独立成为一个rune类型的元素值。
fmt.Printf("runes(char): %q\n", []rune(str)) // ['你' '好' ' ' 'A' 'B' 'C' ' ']
//每个rune类型的值在底层都是由一个 UTF-8 编码值来表达的
//中文字符的UTF-8编码值用三个字节来表达,所以前面两个中文对应的编码值会比较大;
//英文字符的UTF-8编码值用一个字节表达就足够了,所以编码值被转换为整数之后比较小;
fmt.Printf("runes(hex): %x\n", []rune(str)) // [4f60 597d 20 41 42 43 20]
//把每个字符的UTF-8编码值都拆成相应的字节序列
//结果变长了,是因为一个中文字符的UTF-8编码值需要用三个字节来表达。
fmt.Printf("bytes(hex): [% x]\n", []byte(str)) //[e4 bd a0 e5 a5 bd 20 41 42 43 20]
}
func main() {
test1()
}
使用for...range语句遍历字符串的时候,会把字符串的值拆成一个字节序列,然后再找出这个字节序列中包含的每一个 UTF-8 编码值(或者说每一个 Unicode 字符)。如果存在两个迭代变量,那么赋给第一个变量的值 就是当前字节序列中的某个 UTF-8 编码值的第一个字节所对应的那个索引值,赋给第二个变量的值 是这个 UTF-8 编码值代表的那个 Unicode 字符,其类型会是rune。
func test2() {
str := "你好 ABC "
for i, c := range str {
fmt.Printf("%d: %q [%x]\n", i, c, []byte(string(c)))
}
/*
0: '你' [e4 bd a0]
3: '好' [e5 a5 bd]
6: ' ' [20]
7: 'A' [41] //字符是A的十六进制表示为41
8: 'B' [42]
9: 'C' [43]
10: ' ' [20]
*/
}
func main() {
test2()
}
总结:一个string类型的值会由若干个 Unicode 字符组成,每个 Unicode 字符都可以由一个rune类型的值来表达。这些字符在底层都会被转换为 UTF-8 编码值,而这些 UTF-8 编码值又会以字节序列的形式表达和存储。因此,一个string类型的值在底层就是一个能够表达若干个 UTF-8 编码值的字节序列。
Go 语言中的string类型的值是不可变的,如果想得到一个不同的字符串,就只能基于原字符串裁剪(使用切片)或者拼接(操作符+)操作。string值的底层内容会存储到一块连续的内存空间中,也会存储对应的字节数量用来表示该string值的长度。可以把这块内存的内容看成一个字节数组,而相应的string值则包含了指向字节数组头部的指针值。在一个string值上应用切片表达式,就相当于在对其底层的字节数组做切片。在对字符串拼接的时候,会把字符串依次拷贝到一个新的连续内存空间中。一个string值会在底层与它的所有副本共用同一个字节数组,由于这里的字节数组永远不会被改变,所以这样做是绝对安全的。
strings.Builder与string值存储内容的方式是一样的:其中有一个内容容器,是一个byte类型的切片(字节切片),底层数组是一个字节数组,都是通过一个unsafe.Pointer类型的字段指向了底层字节数组的指针值。
strings.Builder类型在使用上有以下约束:被真正使用后就不可以再被复制;需要使用方自行解决操作冲突和并发安全问题。只要调用了Builder值的任意一个方法,这些方法都会改变其所属值中的内容容器的状态,就不能对其所属值复制了,如果在任何副本上调用上述方法都会引发 panic。
func test1() {
//strings.Builder有一个内容容器,是一个byte类型的切片(字节切片),底层数组是一个字节数组,都是通过一个unsafe.Pointer类型的字段指向了底层字节数组的指针值。
var builder1 strings.Builder
builder1.WriteString("你好")
builder1.Write([]byte{'A', 'B', 'C'})
fmt.Println(builder1.String(), builder1.Cap(), builder1.Len()) //你好ABC 16 9
//手动扩容的容量是原容器容量的二倍再加上n
builder1.Grow(10)
fmt.Println(builder1.String(), builder1.Cap(), builder1.Len()) //你好ABC 42 9
//如果在strings.Builder的副本上调用strings.Builder的任意方法都会引发 panic
builder3 := builder1
// builder3.Grow(1) // 这里会引发 panic: strings: illegal use of non-zero Builder copied by value
_ = builder3
//使用Reset方法可以让Builder值重新回到零值状态
builder1.Reset()
fmt.Println(builder1.String(), builder1.Cap(), builder1.Len()) // 0 0
}
可以通过任何方式复制Builder的指针值,这样的指针值指向的都是同一个Builder值。 但是如果Builder值被多方同时操作,那么其中的内容就很可能会产生混乱,也就是容易出现操作冲突和并发安全问题。所以,最好不要共享Builder值以及它的指针值。可以在各处分别声明一个Builder值来使用,也可以先使用再传递,只要在传递之前调用它的Reset方法即可。
func test1() {
//可以通过任何方式复制Builder的指针值,这样的指针值指向的都是同一个Builder值。
f2 := func(bp *strings.Builder) {
(*bp).Grow(1) // 这里虽然不会引发 panic,但不是并发安全的。
builder4 := *bp
//builder4.Grow(1) // 这里会引发 panic。
_ = builder4
}
f2(&builder1)
//如果Builder值被多方同时操作,那么其中的内容就很可能会出现操作冲突和并发安全问题。
ch1 := make(chan strings.Builder, 1)
ch1 <- builder1
builder2 := <-ch1
//builder2.Grow(1) // 这里会引发panic
_ = builder2
//可以先使用再传递,只要在传递之前调用它的Reset方法即可
builder1.Reset()
builder5 := builder1
builder5.Grow(1) // 这里不会引发 panic。
}
【问】strings.Builder类型相比于string值有哪些优势?
【答】strings.Builder类型的值:对于已存在的内容不可变,但可以拼接更多的内容;减少了内存分配和内容拷贝的次数;可将内容重置,可重用值。
strings.Reader类型的值可以高效地读取字符串,在读取的过程中会保存已读取字节的计数,已读计数就是下一次读取的起始索引位置。Reader值的大部分用于读取的方法都会及时地更新已读计数。
func test2() {
// 示例1
reader1 := strings.NewReader("你好ABC")
fmt.Printf("%d,%d\n", reader1.Len(), reader1.Size()) //9,9
buf1 := make([]byte, 47)
n, _ := reader1.Read(buf1)
fmt.Printf("%d,%d,%d\n", n, reader1.Len(), reader1.Size()) //9,0,9
// 示例2
buf2 := make([]byte, 21)
offset1 := int64(64)
n, _ = reader1.ReadAt(buf2, offset1)
fmt.Printf("%d,%d,%d,%d\n", n, offset1, reader1.Len(), reader1.Size()) //0,64,0,9
}
strings包主要面向Unicode字符和经过 UTF-8 编码的字符串,而bytes包面对的主要是字节和字节切片。bytes.Buffer类型主要是作为字节序列的缓冲区,strings.Builder只能拼接和导出字符串,而bytes.Buffer不但可以拼接、截断其中的字节序列,以各种形式导出其中的内容,还可以顺序地读取其中的子序列。bytes.Buffer类型也是使用字节切片作为内容容器,Buffer值的长度是未读内容的长度,而不是已存内容的总长度。Cap方法提供的是内容容器的容量,不是内容长度。
func test1() {
//声明一个bytes.Buffer类型的变量buffer1
var buffer1 bytes.Buffer
//写入一个字符串
buffer1.WriteString("hello")
fmt.Printf("%d,%d\n", buffer1.Len(), buffer1.Cap()) //5,64
//从buffer1中读取一部分内容,并填满长度为7的字节切片p1
p1 := make([]byte, 7)
fmt.Println(p1) //[0 0 0 0 0 0 0]
n, _ := buffer1.Read(p1)
fmt.Printf("%d,%d,%d\n", n, buffer1.Len(), buffer1.Cap()) //5,0,64
}
strings.Builder类型主要用于构建字符串,strings.Reader类型主要用于读取字符串,bytes.Buffer类型主要作为字节序列的缓冲区,它们的指针类型实现的接口都有io包中的接口,这是为了提高不同程序实体之间的互操作性。
func test1() {
// strings.Builder类型实现的io包中的接口
builder := new(strings.Builder)
_ = interface{}(builder).(io.Writer)
_ = interface{}(builder).(io.ByteWriter)
_ = interface{}(builder).(fmt.Stringer)
// strings.Reader类型实现的io包中的接口
reader := strings.NewReader("")
_ = interface{}(reader).(io.Reader)
_ = interface{}(reader).(io.ReaderAt)
_ = interface{}(reader).(io.ByteReader)
_ = interface{}(reader).(io.RuneReader)
_ = interface{}(reader).(io.Seeker)
_ = interface{}(reader).(io.ByteScanner)
_ = interface{}(reader).(io.RuneScanner)
_ = interface{}(reader).(io.WriterTo)
// bytes.Buffer类型实现的io包中的接口
buffer := bytes.NewBuffer([]byte{})
_ = interface{}(buffer).(io.Reader)
_ = interface{}(buffer).(io.ByteReader)
_ = interface{}(buffer).(io.RuneReader)
_ = interface{}(buffer).(io.ByteScanner)
_ = interface{}(buffer).(io.RuneScanner)
_ = interface{}(buffer).(io.WriterTo)
_ = interface{}(buffer).(io.Writer)
_ = interface{}(buffer).(io.ByteWriter)
_ = interface{}(buffer).(io.ReaderFrom)
_ = interface{}(buffer).(fmt.Stringer)
}
在io包中,有这样几个用于拷贝数据的函数:io.Copy;io.CopyBuffer;io.CopyN,这些函数的功能都是把数据从src拷贝到dst。
func test2() {
//使用strings.NewReader创建了一个字符串读取器,并赋给了变量src
src := strings.NewReader("Go语言标准库常用的代码包")
//new一个字符串构建器,并将其赋予了变量dst
dst := new(strings.Builder)
//调用io.CopyN函数,把这两个变量的值都传进去,从src中拷贝前8个字节到dst那里。
written, err := io.CopyN(dst, src, 8)
if err != nil {
fmt.Printf("error: %v\n", err)
} else {
fmt.Printf("Written(%d): %q\n", written, dst.String()) //Written(8): "Go语言"
}
}
虽然变量src和dst的类型分别是strings.Reader和strings.Builder,但是当它们被传到io.CopyN函数的时候,就已经分别被包装成了io.Reader类型和io.Writer类型的值。io.CopyN函数也不会在意它们的实际类型到底是什么。
在 《Go语言中使用组合来实现“继承”》 中说过,Go 语言中通过接口类型之间的嵌入来实现组合和扩展,比如在io包中,io.Reader的扩展接口有下面几种:
//$GOROOT/src/io/io.go
//ReadWriter既是io.Reader的扩展接口,也是io.Writer的扩展接口。
//该接口定义了一组行为,仅包含了基本的字节序列读取方法Read 和字节序列写入方法Write。
type ReadWriter interface {
Reader
Writer
}
//ReadCloser接口除了包含基本的字节序列读取方法之外,还拥有一个基本的关闭方法Close。
//Close一般用于关闭数据读写的通路;这个接口其实是io.Reader接口和io.Closer接口的组合。
type ReadCloser interface {
Reader
Closer
}
// 此接口是io.Reader、io.Writer、io.Closer 这三个接口的组合。
type ReadWriteCloser interface {
Reader
Writer
Closer
}
// 此接口的特点是拥有一个用于寻找读写位置的基本方法Seek,该方法是io.Seeker接口唯一拥有的方法。
type ReadSeeker interface {
Reader
Seeker
}
// 此接口是io.Reader、io.Writer、io.Seeker的组合。
type ReadSeekCloser interface {
Reader
Seeker
Closer
}
io包中的io.Reader接口的实现类型包括下面几项内容:
//$GOROOT/src/io/io.go
// 此类型的读取方法Read返回的总数据量会受到限制,无论该方法被调用多少次,这个限制由该类型的字段N指明,单位是字节。
type LimitedReader struct {
R Reader // underlying reader
N int64 // max bytes remaining
}
// 此类型的基本类型可以包装io.ReaderAt类型的值,并且会限制它的Read方法,
// 该类型值的行为与切片有些类似,它只会对外暴露在其窗口之中的那些数据。
type SectionReader struct {
r ReaderAt
base int64
off int64
limit int64
}
// 此类型是一个包级私有的数据类型,也是io.TeeReader函数结果值的实际类型。
// 这个函数接受两个参数r和w,类型分别是io.Reader和io.Writer。
// 其结果值的Read方法会把r中的数据经过作为方法参数的字节切片p写入到w。
type teeReader struct {
r Reader
w Writer
}
io包中的核心接口只有 3 个:io.Reader、io.Writer、io.Closer。 io包中的接口主要针对四种操作:读取、写入、关闭、读写位置设定,前三种操作属于基本的 I/O 操作。
在io包中,与写入操作有关的接口都与读取操作的相关接口有着一定的对应关系。写入操作相关的接口如下:
bufio是“buffered I/O”的缩写,这个代码包中的程序实体实现的 I/O 操作都内置了缓冲区。bufio包中的数据类型主要有:Reader;Scanner;Writer;ReadWriter。bufio.Reader类型的值内的缓冲区其实是一个数据存储中介,它介于底层读取器与读取方法及其调用方之间。这里的底层读取器就是在初始化此类值的时候传入的io.Reader类型的参数值。
Reader值的读取方法一般都会先从其所属值的缓冲区中读取数据,在必要的时候还会预先从底层读取器那里读出一部分数据并暂存于缓冲区之中以备后用。有这样一个缓冲区的好处是,可以在大多数的时候降低读取方法的执行时间。虽然读取方法有时还要负责填充缓冲区,但从总体来看,读取方法的平均执行时间一般都会因此有大幅度的缩短。
//$GOROOT/bufio/bufio.go
type Reader struct {
buf []byte //字节切片,代表缓冲区。虽然它是切片类型的,但是其长度却会在初始化的时候指定,并在之后保持不变。
rd io.Reader //代表底层读取器。缓冲区中的数据就是从这里拷贝来的。
r, w int //代表对缓冲区进行下一次 读取/写入 时的开始索引,可以称它为已读计数。
err error //用于表示在从底层读取器获得数据时发生的错误。这里的值在被读取或忽略之后,该字段会被置为nil。
lastByte int //用于记录缓冲区中最后一个被读取的字节,读回退时会用到它的值。
lastRuneSize int //用于记录缓冲区中最后一个被读取的 Unicode 字符所占用的字节数,读回退的时候会用到它的值。这个字段只会在其所属值的ReadRune方法中才会被赋予有意义的值,在其他情况下,它都会被置为-1。
//bufio包提供了两个用于初始化Reader值的函数
//NewReader函数初始化的Reader值会拥有一个默认尺寸的缓冲区。这个默认尺寸是 4096 个字节,即:4 KB
func NewReader(rd io.Reader) *Reader {
return NewReaderSize(rd, defaultBufSize)
}
//NewReaderSize函数则将缓冲区尺寸的决定权抛给了使用方。
func NewReaderSize(rd io.Reader, size int) *Reader {
}
bufio.Reader类型有 4 个用于读取数据的指针方法:Peek、Read、ReadSlice、ReadBytes,需要注意Peek方法、ReadSlice方法、ReadLine方法 都有可能会造成内容泄露,因为它们在正常的情况下都会返回直接基于缓冲区的字节切片。
func test1() {
comment := "this is a long long text,because i will use it to test something ..."
basicReader := strings.NewReader(comment)
fmt.Println(basicReader.Size()) //68
// 示例1: 此时reader1的缓冲区还没有被填充
reader1 := bufio.NewReader(basicReader)
fmt.Println(reader1.Size(), reader1.Buffered()) //4096 0
// 示例2: Peek方法 读取并返回其缓冲区中的n个未读字节
bytes, err := reader1.Peek(7)
if err != nil {
fmt.Printf("error: %v\n", err)
}
fmt.Println(len(bytes), bytes, string(bytes)) //7 [116 104 105 115 32 105 115] this is
fmt.Println(reader1.Size(), reader1.Buffered()) //4096 68
// 示例3: Read方法 把缓冲区中的未读字节,依次拷贝到其参数p代表的字节切片中
buf1 := make([]byte, 7)
n, err := reader1.Read(buf1)
if err != nil {
fmt.Printf("error: %v\n", err)
}
fmt.Println(n, buf1, string(buf1)) //7 [116 104 105 115 32 105 115] this is
fmt.Println(reader1.Size(), reader1.Buffered()) //4096 61
}
os代码包中的 API可以操控计算机操作系统,可以使用操作系统中的文件系统、权限系统、环境变量、系统进程、系统信号。
os.File类型代表了操作系统中的文件,拥有的都是指针方法,除了空接口之外,它本身没有实现任何接口。而它的指针类型则实现了很多io代码包中的接口,包括:io.Reader、io.Writer、io.Closer、io.ReaderAt、io.Seeker、io.WriterAt。在os包中有这样几个函数:Create、NewFile、Open和OpenFile。
//$GOROOT/src/os/file.go
//用于根据给定的路径创建一个新的文件,它会返回一个File值和一个错误值
func Create(name string) (*File, error) {
}
//并不是创建一个新的文件,而是依据一个已经存在的文件的描述符,来新建一个包装了该文件的File值。
//需要接受一个代表文件描述符的、uintptr类型的值,以及一个用于表示文件名的字符串值。
func NewFile(fd uintptr, name string) *File {
}
//打开一个文件并返回包装了该文件的File值,该函数只能以只读模式打开文件。
//只能从该函数返回的File值中读取内容,而不能向它写入任何内容。
func Open(name string) (*File, error) {
}
//是os.Create函数和os.Open函数的底层支持,它最为灵活。
//name是文件的路径,flag是需要施加在文件描述符之上的模式
func OpenFile(name string, flag int, perm FileMode) (*File, error) {
}
针对File值的操作模式有只读模式、只写模式、读写模式,这些模式分别由常量os.O_RDONLY、os.O_WRONLY、os.O_RDWR代表。在新建或打开一个文件的时候,必须把这三个模式中的一个设定为此文件的操作模式。另外还可以设置额外的操作模式,可选项如下所示:
更多使用细节参考:Go语言学习笔记—golang标准库os包
源代码:https://gitee.com/rxbook/go-demo-2023/tree/master/basic/go04/tools