io包和操作
Some checks failed
Release / deploy (push) Failing after 1m40s

This commit is contained in:
王一之 2024-03-23 18:09:44 +08:00
parent 71f300dbca
commit ac55a0c6a2
7 changed files with 330 additions and 242 deletions

View File

@ -1,129 +0,0 @@
package main
import (
"bytes"
"crypto/md5"
"fmt"
"io"
"net/http"
"runtime"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
/**
1G 文件测试结果
{
"md5": "23023b24fc0ab2d03d5cd62a18bb4aa8",
"mem": "96M",
"startMem": "1M"
}
100M 文件测试结果
{
"md5": "6ba2b5a1b62f356fc86efad690613916",
"mem": "96M",
"startMem": "0M"
}
*/
r.GET("/hash1", func(c *gin.Context) {
fmt.Printf("%v", c.ContentType())
m := runtime.MemStats{}
runtime.ReadMemStats(&m)
startM := m.Alloc / 1024 / 1024
oldBody := c.Request.Body
defer oldBody.Close()
pr, pw := io.Pipe()
defer pw.Close()
defer pr.Close()
c.Request.Body = pr
hash := md5.New()
go func() {
_, err := io.Copy(io.MultiWriter(hash, pw), oldBody)
if err != nil {
fmt.Printf("io copy: %v", err)
}
}()
_, err := c.MultipartForm()
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
runtime.ReadMemStats(&m)
c.JSON(http.StatusOK, gin.H{
"md5": fmt.Sprintf("%x", hash.Sum(nil)),
"startMem": fmt.Sprintf("%dM", startM),
"mem": fmt.Sprintf("%dM", m.Alloc/1024/1024),
})
})
/**
1G 文件测试结果
{
"md5": "62da2c499cdb3fad927f881c134684b0",
"mem": "2922M",
"startMem": "1M"
}
100M 文件测试结果
{
"md5": "55a6849293d0847a48f856254aa910e2",
"mem": "341M",
"startMem": "1M"
}
*/
r.GET("/hash2", func(c *gin.Context) {
m := runtime.MemStats{}
runtime.ReadMemStats(&m)
startM := m.Alloc / 1024 / 1024
oldBody := c.Request.Body
defer oldBody.Close()
buffer := bytes.NewBuffer(nil)
hash := md5.New()
_, err := io.Copy(io.MultiWriter(buffer, hash), oldBody)
if err != nil {
fmt.Printf("io copy2: %v", err)
}
c.Request.Body = io.NopCloser(buffer)
_, err = c.MultipartForm()
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
runtime.ReadMemStats(&m)
c.JSON(http.StatusOK, gin.H{
"md5": fmt.Sprintf("%x", hash.Sum(nil)),
"startMem": fmt.Sprintf("%dM", startM),
"mem": fmt.Sprintf("%dM", m.Alloc/1024/1024),
})
})
/**
1G 文件测试结果
{
"mem": "96M",
"startMem": "1M"
}
100M 文件测试结果
{
"mem": "96M",
"startMem": "1M"
}
*/
r.GET("/hash3", func(c *gin.Context) {
m := runtime.MemStats{}
runtime.ReadMemStats(&m)
startM := m.Alloc / 1024 / 1024
_, err := c.MultipartForm()
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
runtime.ReadMemStats(&m)
c.JSON(http.StatusOK, gin.H{
"startMem": fmt.Sprintf("%dM", startM),
"mem": fmt.Sprintf("%dM", m.Alloc/1024/1024),
})
})
r.Run(":8088")
}

View File

@ -0,0 +1,75 @@
package code
import (
"bufio"
"bytes"
"crypto/md5"
"crypto/sha1"
"io"
"testing"
)
func TestIOHash(t *testing.T) {
file := bytes.NewBuffer([]byte("hello world"))
md5 := md5.New()
sha1 := sha1.New()
r := io.TeeReader(file, io.MultiWriter(md5, sha1))
_, _ = io.Copy(io.Discard, r)
t.Logf("md5: %x\n", md5.Sum(nil))
t.Logf("sha1: %x\n", sha1.Sum(nil))
}
func TestLimitReader(t *testing.T) {
file := bytes.NewBuffer([]byte("hello world"))
lr := io.LimitReader(file, 5)
data, _ := io.ReadAll(lr)
t.Logf("%s\n", data)
}
func TestBufIo(t *testing.T) {
// 写入100M文件
file := bytes.NewBufferString("hello world\ngolang")
buf := bufio.NewReader(file)
for {
line, err := buf.ReadString('\n')
if err == io.EOF {
t.Logf("%s", line)
break
}
t.Logf("%s", line)
}
}
// 凯撒加密
type decode struct {
r io.Reader
}
func (d *decode) Read(p []byte) (int, error) {
// 读取数据
n, err := d.r.Read(p)
if err != nil {
if err != io.EOF {
return n, err
}
}
// 解密
for i := 0; i < n; i++ {
p[i]++
}
return n, err
}
func TestDecodePipe(t *testing.T) {
body := bytes.NewBuffer([]byte("gdkkn\u001Fvnqkc"))
r, w := io.Pipe()
defer r.Close()
go func() {
defer w.Close()
// 读取加密数据解密
_, _ = io.Copy(w, &decode{r: body})
}()
// 读取解密数据,保存文件
data, _ := io.ReadAll(r)
t.Logf("%s\n", data)
}

View File

@ -2,10 +2,11 @@ package code
import (
"fmt"
"github.com/stretchr/testify/assert"
"reflect"
"testing"
"unsafe"
"github.com/stretchr/testify/assert"
)
func TestSliceArray(t *testing.T) {

View File

@ -0,0 +1,148 @@
# Golang IO相关操作
- io包提供了基础的io操作几乎所有的io操作都是基于io.Reader和io.Writer接口的。
- ioutil 包提供了一些方便的io操作函数但是在go 1.16中已经被废弃放进了io包。
- bufio实现了缓冲io可以提高io效率。
- bytes 包中提供了Buffer类型可以用来做io操作。
## 核心接口
### Reader/Writer/Seeker
Reader和Writer是io包中最重要的接口它们定义了io操作的基本行为。像一些文件操作网络操作等等都是基于这两个接口的。
Seeker接口定义了Seek方法可以在Reader/Writer中定位到指定的位置文件操作中常用。
## 常用函数
下面我们来看一些常用的io操作函数更多的函数可以查看官方文档我只是列出一些我常用到的。
- io.ReadeAll 读取所有数据返回一个bytes
- io.MultiWriter/Reader 可以将多个Reader/Writer合并成一个
- io.LimitReader 限制Reader的读取长度读取n个长度后返回io.EOF
- io.TeeReader 读取数据的同时写入到另一个Writer中
- io.Pipe 创建同步内存管道
- io.Copy/CopyN 复制Reader到Writer
- io.NopCloser 给一个io.Reader加上Close方法
- bytes.NewBuffer 创建一个Buffer
- bufio.NewReader/Writer 创建一个缓冲Reader/Writer
## 常见小坑
> 我暂时能想到的坑记录下来,欢迎补充
### io.EOF
io.EOF是io包中定义的一个错误表示读取到文件末尾。在读取文件时当读取到文件末尾时会返回io.EOF错误。
请注意当读取到文件末尾时返回的数据可能不是nil而是文件的最后一部分数据需要注意处理。
### 大文件的操作
在处理大文件时需要注意内存的使用尽量使用io.Reader和io.Writer来处理文件避免一次性读取整个文件到内存中。
### bufio.Writer.Flush
在使用bufio.Writer时需要注意Flush方法因为bufio.Writer是带缓冲的数据并不是实时写入到Writer中的需要调用Flush方法来刷新缓冲区。
### io.Pipe
io.Pipe是一个同步的内存管道请注意在使用时Reader与Writer必须在不同的goroutine中且需要注意Reader与Writer的速度需要一致否则会造成死锁或线程阻塞。
并且需要使用Close方法来关闭Reader或Writer否则会造成内存泄漏。
## 高级用例
下面介绍一些高级用例你也可以从中学习到一些高级的io操作技巧。
### 计算上传文件的hash
下面的例子是计算上传文件的md5和sha1的hash值我们使用io.TeeReader将文件内容同时写入到md5和sha1的hash计算器中。
像我们在http上传文件时可以在上传的同时计算文件的hash值这样可以保证文件的完整性。同时也可以打开文件再将文件的Writer放到io.Copy中这样可以同时写入文件和计算hash值。
```go
func TestIOHash(t *testing.T) {
file := bytes.NewBuffer([]byte("hello world"))
md5 := md5.New()
sha1 := sha1.New()
r := io.TeeReader(file, io.MultiWriter(md5, sha1))
_, _ = io.Copy(io.Discard, r)
t.Logf("md5: %x\n", md5.Sum(nil))
t.Logf("sha1: %x\n", sha1.Sum(nil))
}
```
### 读取文件的部分内容
下面的例子是读取文件的部分内容我们使用io.LimitReader限制读取的长度读取n个长度后返回io.EOF这在解析文件头部信息时非常有用。
```go
func TestLimitReader(t *testing.T) {
file := bytes.NewBuffer([]byte("hello world"))
lr := io.LimitReader(file, 5)
data, _ := io.ReadAll(lr)
t.Logf("%s\n", data)
}
```
### 大文件按行读取
下面的例子是读取大文件的每一行我们使用bufio.Reader来读取文件的每一行这样可以避免一次性读取整个文件到内存中。
在读一些io比较慢的文件时网络文件也可以使用这种方式来读取文件。写文件也是类似的可以使用bufio.Writer来写文件提高磁盘io效率避免频繁的io操作。
```go
func TestBufIo(t *testing.T) {
file := bytes.NewBufferString("hello world\ngolang")
buf := bufio.NewReader(file)
for {
line, err := buf.ReadString('\n')
if err == io.EOF {
t.Logf("%s", line)
break
}
t.Logf("%s", line)
}
}
```
### 使用Pipe将上传文件解密写入文件
有时候上传的文件是一个加密的我们需要在上传的同时解密文件然后同时写入到磁盘中这时候可以使用io.Pipe来实现。
```go
// 凯撒加密
type decode struct {
r io.Reader
}
func (d *decode) Read(p []byte) (int, error) {
// 读取数据
n, err := d.r.Read(p)
if err != nil {
if err != io.EOF {
return n, err
}
}
// 解密
for i := 0; i < n; i++ {
p[i]++
}
return n, err
}
func TestDecodePipe(t *testing.T) {
body := bytes.NewBuffer([]byte("gdkkn\u001Fvnqkc"))
r, w := io.Pipe()
defer r.Close()
go func() {
defer w.Close()
// 读取加密数据解密
_, _ = io.Copy(w, &decode{r: body})
}()
// 读取解密数据,保存文件
data, _ := io.ReadAll(r)
t.Logf("%s\n", data)
}
```

View File

@ -4,7 +4,7 @@ title: Go 切片、数组、字符串
## 切片和数组
切片由三个部分组成:指针(指向底层数组),长度(当前切片使用的长度),容量(切片能包含多少个成员),可以看`reflect.SliceHeader`的定义不过这个结构体已经在1.20标注为废弃。
切片由三个部分组成:指针(指向底层数组),长度(当前切片使用的长度),容量(切片能包含多少个成员),可以看`reflect.SliceHeader`的定义不过这个结构体已经在1.20标注为废弃这个废弃应该不算真废弃而是不推荐使用官方曾在1.18、1.19反复横跳过
当调用一个函数的时候,函数的每个调用参数将会被赋值给函数内部的参数变量,所以函数参数变量接收的是一个复制的副本,并不是原始调用的变量。(所以数组作为参数,是低效的,还需要进行一次数组的拷贝,可以使用数组指针)
@ -41,7 +41,7 @@ func ModifySlice(slice []int) {
`切片是由:数组指针,长度,容量组成的`,来划一下重点.
副本传的也是上面这些东西.然后修改切片的时候呢,实际上是通过切片里面的数组指针去修改了,并没有修改切片的值(数组指针,长度,容量).我们可以用unsafe包来看看切片与数组的结构
```
```go
func TestSlice(t *testing.T) {
slice := []int{1, 2, 3}
arr := [...]int{1, 2, 3}
@ -168,15 +168,13 @@ func ModifySlice(slice []int) {
我把之前的`ModifySlice`方法修改了一下,然后成员没加,后面再修改回去为 3 也没有发生变化了.
这是因为 append 的时候因为容量不够扩容了,导致底层数组指针发生了改变,但是传进来的切片是外面切片的副本,修改这个切片里面的数组指针不会影响到外面的切片
## 切片扩容
在1.18之前切片的扩容是原来的2倍但是当容量超过1024时每次容量变成原来的1.25倍直到大于期望容量。在1.18后更换了新的机制:
https://github.com/golang/go/blob/27f41bb15391668fa8ba18561efe364bab9b8312/src/runtime/slice.go#L267
<https://github.com/golang/go/blob/27f41bb15391668fa8ba18561efe364bab9b8312/src/runtime/slice.go#L267>
- 当新切片>旧切片*2时直接安装新切片容量计算
- 当新切片>旧切片\*2时直接安装新切片容量计算
- 如果旧切片\<256新切片容量为旧切片\*2
- 如果旧切片>=256`newcap+=(newcap + 3*threshold) >> 2`循环计算1.25倍-2倍平滑增长直到大于或者等于目标容量
@ -197,7 +195,7 @@ func TestCap(t *testing.T) {
至于实际的结果为什么没有和上述说的一样,可以看到,在`nextslicecap`计算出容量后续,还有对`newcap`的一系列操作,这是内存对齐的一系列计算。
https://github.com/golang/go/blob/27f41bb15391668fa8ba18561efe364bab9b8312/src/runtime/slice.go#L188
<https://github.com/golang/go/blob/27f41bb15391668fa8ba18561efe364bab9b8312/src/runtime/slice.go#L188>
逻辑比较复杂,可以进入调试模式跟踪逻辑,这里就不多展开了
@ -205,8 +203,6 @@ https://github.com/golang/go/blob/27f41bb15391668fa8ba18561efe364bab9b8312/src/r
而且我按照最新的源码来看与网上大多数的教程说得也有出入网上很多人都是直接说的1.25倍增长也许1.18时是这样的这个我就没有探究了在现在1.22已经修改成为了上述的公示可以在1.25-2倍之间平滑增长。
## 字符串
字符串是一种特殊的切片在go中是一个不可变的字节序列和数组不同的是字符串的元素不可修改可以从`reflect.StringHeader`看到它的底层结构由一个len长度与data数组指针组成。
@ -247,6 +243,8 @@ func TestStringAsSlice(t *testing.T) {
一些高性能的编程技巧,其实大多都是为了避免内存拷贝而产生的性能消耗,以下是我想到的几种场景,以供参考,也欢迎指教。
注意:其中使用了`unsafe`包,这个包是不安全的,请谨慎使用。
### 零拷贝
在进行string->bytes的转换时使用零拷贝否则会产生一次内存拷贝你可以写一个Benchmark来对比一下
@ -275,8 +273,6 @@ func TestConvertZeroCopy(t *testing.T) {
}
```
### 字符串高性能替换
```go
@ -284,11 +280,9 @@ func TestReplace(t *testing.T) {
str := "hello,world"
fmt.Printf("%v %+v\n", str, (*reflect.StringHeader)(unsafe.Pointer(&str)))
b := []byte(str)
// 大量字符串操作
b[1] = 'a'
str = unsafe.String(unsafe.SliceData(b), len(b))
fmt.Printf("%v %+v\n", str, (*reflect.StringHeader)(unsafe.Pointer(&str)))
}
```

View File

@ -1 +0,0 @@
# Golang IO包

View File

@ -54,7 +54,7 @@ Docusaurus 是有 blog 功能的,但是 blog 不能支持左侧的目录树,
- 增加 Docs 文章时间排序
- 接入 giscus 评论
- 接入 Google Analytics 统计
- 增加了 Markdown lint(vscode 插件)
- 增加了 Markdown lint 校验与格式化(vscode 插件)
后续或许还会写一个后端,来实现其他更多的功能。