传统的IO流程
在传统的IO流程中,通常涉及以下几个步骤:
- 打开文件或建立网络连接:首先,需要打开文件或建立网络连接,以便进行读取或写入操作。这通常涉及到操作系统提供的系统调用,如
open
、socket
等。 - 读取或写入数据:一旦文件或网络连接打开,就可以进行数据的读取或写入操作。读取操作将从文件或网络连接中获取数据,而写入操作将将数据写入文件或发送到网络连接中。这些操作通常涉及到系统调用,如
read
和write
。 - 缓冲区:为了提高IO性能,通常会使用缓冲区。缓冲区是一块内存区域,用于临时存储要读取或写入的数据。数据首先被读取到缓冲区中,然后从缓冲区中写入到文件或网络连接中,或者从缓冲区中读取数据。
- 关闭文件或网络连接:当读取或写入操作完成后,需要关闭文件或网络连接。这通常涉及到系统调用,如
close
。
在传统的IO流程中,每次读取或写入操作都会涉及到系统调用,这会导致较高的开销。为了提高性能,通常会使用缓冲区来减少系统调用的次数。缓冲区可以一次读取或写入多个数据,从而减少了系统调用的开销。
缓冲区
上面我们了解到缓冲区这个概念,那什么是缓冲区呢?
内存缓冲区是计算机中的一种临时存储区域,用于暂时存储数据。它通常用于提高数据读写的效率,减少对底层存储设备的频繁访问。
缓冲区的主要目的是在数据的生产者和消费者之间起到一个中间层的作用。当数据被生产者生成时,它首先被写入缓冲区,而不是直接写入到目标存储设备。然后,消费者可以从缓冲区中读取数据。
缓冲区的大小是有限的,一旦缓冲区被填满,生产者必须等待消费者读取数据,以便为新的数据腾出空间。同样,如果缓冲区为空,消费者必须等待生产者生成新的数据。
内存缓冲区可以用于各种场景,比如:
- 文件读写:在读取或写入大文件时,可以使用内存缓冲区来提高读写性能。数据首先被读取到缓冲区中,然后批量地写入或读取到磁盘上的文件。
- 网络通信:在网络通信中,数据通常需要经过网络协议的封装和解析。使用内存缓冲区可以将数据暂时存储起来,以便进行协议处理和网络传输。
- 数据库操作:在数据库操作中,使用内存缓冲区可以提高数据的读写性能。数据首先被写入缓冲区,然后批量地写入到数据库中,或者从缓冲区中读取数据进行查询。
需要注意的是,内存缓冲区只是一个临时存储区域,数据在缓冲区中并不是持久化的。一旦程序结束或缓冲区被清空,缓冲区中的数据就会丢失。因此,在使用内存缓冲区时需要确保数据的正确性和一致性。
go缓冲区
在Go语言中,缓冲区的大小是由创建缓冲区时指定的参数决定的。在标准库中,可以使用bufio
包提供的NewWriterSize
函数创建一个指定大小的缓冲区。
默认情况下,bufio.Writer
的缓冲区大小为4096字节(4KB),即调用bufio.NewWriter
创建的缓冲区大小为4096字节。这是因为4096字节是一个常见的磁盘块大小,对于大多数应用场景来说,这个大小已经足够了。
如果需要自定义缓冲区的大小,可以使用bufio.NewWriterSize
函数来指定缓冲区的大小。例如,可以通过bufio.NewWriterSize(writer, 8192)
来创建一个大小为8192字节(8KB)的缓冲区。
为什么
为什么go编程中要设置缓冲区呢?其实我们上面都有提到:设置缓冲区的一个主要目的就是为了减少频繁的IO操作。
在进行IO操作时,例如读取或写入文件,每次都直接操作底层的存储设备(如磁盘或网络)可能会导致性能下降。这是因为每次IO操作都需要进行系统调用,这涉及到内核和用户空间之间的上下文切换,以及硬件设备的访问延迟。
通过使用缓冲区,可以将数据暂时存储在内存中,而不是直接与底层存储设备进行交互。这样可以将多个小的IO操作合并为一个大的IO操作,从而减少了系统调用的次数。这种批量处理的方式通常比频繁的小IO操作更高效。
此外,缓冲区还可以提供更好的数据传输效率。当数据被写入缓冲区时,实际的IO操作可以被推迟到缓冲区被填满或手动刷新缓冲区时才执行。这样可以减少IO操作的次数,提高数据传输的效率。
go 缓冲区(Buffer)是分配在堆还是栈?
在Go语言中,缓冲区(Buffer)的申请是在堆上进行的。
Go语言中的栈空间是有限的,而且栈上的内存分配和释放是由编译器自动管理的,无法手动控制。因此,较大的缓冲区无法放在栈上进行申请。
相反,Go语言中的堆空间是用于动态分配内存的,可以手动控制内存的申请和释放。当我们使用make
关键字创建一个切片或映射时,内存就会在堆上进行动态分配。而bufio
包中的缓冲区也是通过make
函数在堆上进行申请的。
缓冲区的申请通常是在创建缓冲区时进行的,例如使用bufio.NewWriter
或bufio.NewWriterSize
函数来创建一个缓冲区对象。这个过程会调用make
函数来分配足够大小的内存,并返回一个指向该内存的指针。
fmt打印
示例
fmt.Println("Hello, world!"),大家平时用得最多了,这不就是打印输出到控制台嘛
当执行fmt.Println("Hello, world!")
命令时,会调用fmt
包内的Println
函数来打印输出。
首先,Println
函数会根据传入的参数列表构建一个字符串,并将其传递给Fprintln
函数。Fprintln
函数是fmt
包内部的一个辅助函数,它会将构建的字符串写入到标准输出(即控制台)。
在Fprintln
函数内部,它会调用newPrinter
函数来创建一个pp
(printer)对象。pp
对象是printer
结构体的实例,它包含了打印输出的相关配置和状态信息。
接下来,Fprintln
函数会调用pp.print
方法来实际执行打印输出的操作。在print
方法中,它会根据配置的格式化选项,将构建的字符串写入到pp.buf
缓冲区中。
如果缓冲区已满,或者遇到换行符(\n
),print
方法会调用pp.write
方法将缓冲区的内容写入到标准输出。write
方法会使用os.Stdout
作为目标,将缓冲区的内容写入到控制台。
最后,Fprintln
函数会调用pp.free
方法来释放pp
对象占用的内存,以及清空缓冲区。
总结起来,当执行fmt.Println("Hello, world!")
命令时,fmt
包内部会构建打印输出的字符串,并将其写入到标准输出。这个过程涉及到字符串的构建、缓冲区的管理和标准输出的写入。通过使用printer
结构体和相关方法,fmt
包实现了方便的打印输出功能。
源码查看
go
// ...
func main() {
fmt.Println("Hello, world!")
}
// fmt 包
// ...
func Println(a ...any) (n int, err error) {
return Fprintln(os.Stdout, a...)
}
// ...
func Fprintln(w io.Writer, a ...any) (n int, err error) {
p := newPrinter()
p.doPrintln(a)
n, err = w.Write(p.buf)
p.free()
return
}
当打印内容很大怎么办?
当打印的内容超出了缓冲区的大小时,fmt
包会动态扩展缓冲区的大小,以容纳更大的数据,并完整地输出到标准输出。
在执行打印操作时,fmt
包会检查缓冲区的剩余空间是否足够容纳当前要打印的内容。如果空间不足,fmt
包会自动扩展缓冲区的大小,通常是将缓冲区的大小翻倍。
例如,在默认情况下,fmt
包的缓冲区大小为4KB。如果要打印的内容超过了4KB,fmt
包会自动将缓冲区扩展到8KB,以容纳更多的数据。如果仍然不够,会继续扩展到16KB,以此类推,直到能够容纳所有的数据。
当缓冲区大小足够容纳要打印的内容时,fmt
包会将数据写入缓冲区中。当缓冲区满了或者遇到换行符(\n
)时,fmt
包会将缓冲区的内容写入到标准输出,确保完整地输出所有的数据。
也就是说我有一个8k的打印内容,而缓冲区大小为4KB,那么在第一次写入缓冲区后,缓冲区将被填满。此时,fmt
包会进行一次IO操作,将缓冲区的内容写入标准输出。
由于缓冲区已满,第二次写入操作将触发另一次IO操作,将剩余的内容写入标准输出。
因此,在这种情况下,fmt
包会进行两次IO操作,将完整的8KB内容写入标准输出。第一次是在缓冲区填满后,第二次是在第二次写入时。