为了账号安全,请及时绑定邮箱和手机立即绑定

为什么这个 Golang 代码会泄漏内存中变量的值

为什么这个 Golang 代码会泄漏内存中变量的值

Go
HUH函数 2023-07-26 19:37:22
此代码可能会泄漏内存中变量的值。我想也许fmt.XprintY不会重置缓冲区,但我的调试尝试是徒劳的。package mainimport (    "bytes"    "fmt"    "io"    "text/template")type SecWriter struct {    w io.Writer}func (s *SecWriter) Write(p []byte) (n int, err error) {    fmt.Println(string(p), len(p), cap(p))    // here    tmp := fmt.Sprintln("info{SSSSSSSSSSSSSSSSSSSSSSSSSSS}")    if tmp == ""{}    s.w.Write(p[:64])    return 64, nil}func index() {    exp := "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA{{1}}"    b := &bytes.Buffer{}    s := &SecWriter{        w: b,    }    t := template.Must(template.New("index").Parse(exp))    t.Execute(s, nil)    fmt.Println("buf: ", b.String())}func main() {    index()}我的go env:set GOARCH=amd64set GOOS=windows去版本go version go1.12.5 windows/amd64输出是:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 64 641 1 128buf: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1nfo{SSSSSSSSSSSSSSSSSSSSSSSSSSS}                 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA可以看到,内存中变量的部分值:tmp := fmt.Sprintln("info{SSSSSSSSSSSSSSSSSSSSSSSSSSS}")泄漏到缓冲区。
查看完整描述

3 回答

?
繁华开满天机

TA贡献1816条经验 获得超4个赞

没有内存泄漏
但有一个问题p := newPrinter():对initialized的调用p.fmt.init(&p.buf)返回func Fprint(w io.Writer, a ...interface{}) (n int, err error)空闲内存(切片的底层数组),而没有将其初始化为零(可能是出于性能原因而未初始化 -我们期望全部为零)。

TL;DR:
两种解决方案:
1. 解决方法:使用s.w.Write(p)代替s.w.Write(p[:64]),或编辑代码并将其p[len(p):cap(p)]全部设置为零(如果您不这样做或无法触及第二个解决方案):

func (s *SecWriter) Write(p []byte) (n int, err error) {

    b := p[len(p):cap(p)]

    for i := range b {

        b[i] = 0

    }

    fmt.Println(string(p), len(p), cap(p))

    // here

    tmp := fmt.Sprintln("info{SSSSSSSSSSSSSSSSSSSSSSSSSSS}")

    if tmp == "" {

    }

    s.w.Write(p[:64])

    return 64, nil

}

在 Windows ( C:\Go\src\fmt\format.go) 或 Linux ( /usr/local/go/src/fmt/format.go) 文件中,第 58 行将缓冲区设置为全零:

    b := (*buf)[:cap(*buf)]

    for i := range b {

        b[i] = 0

    }

在这个函数内部:


func (f *fmt) init(buf *buffer) {

    b := (*buf)[:cap(*buf)]

    for i := range b {

        b[i] = 0

    }

    f.buf = buf

    f.clearflags()

}

应用此功能后的代码输出:


AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 64 64

1 1 128

buf:  AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1

长答案:
您正在观看超出指定长度的切片数据,并且允许您查看切片容量的切片数据。
您可以将:替换m.Writer.Write(p[:8])为:m.Writer.Write(p),这将使您的代码正常工作。以下代码显示将template.Parse()模板标记为 3 部分并调用my.Write()3 次。这里有趣的部分是第二次调用显示my.Write()编译器生成的具有不同切片容量的切片,该切片未初始化为零,“也许这是一个无害的小问题”:

如果您想监视计算机的内存,请尝试以下操作:

package main


import (

    "bytes"

    "fmt"

    "io"

    "text/template"

)


func main() {

    buf := &bytes.Buffer{}

    my := &myWriter{"You", buf}

    template.Must(template.New("my").Parse("Hi{{.Name}}Bye.")).Execute(my, my)

    fmt.Printf("<<%q>>\n", buf.String())

}

func (m *myWriter) Write(p []byte) (n int, err error) {

    fmt.Printf("len=%v cap=%v\t%v %v\n", len(p), cap(p), string(p), p[:cap(p)])

    no++

    fmt.Println("gen:", no, gen())

    m.Writer.Write(p)

    // m.Writer.Write(p[:8])

    return 8, nil

}


type myWriter struct {

    Name string

    io.Writer

}


const genLen = 8


func gen() string {

    b := [genLen]byte{}

    for i := range b {

        b[i] = no

    }

    return string(b[:])

}


var no = byte(49) //'1'

输出:


len=2 cap=8 Hi [72 105 0 0 0 0 0 0]

gen: 50 22222222

len=3 cap=64    You [89 111 117 58 32 53 48 32 50 50 50 50 50 50 50 50 10 50 32 49 48 53 32 48 32 48 32 48 32 48 32 48 32 48 93 10 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]

gen: 51 33333333

len=4 cap=8 Bye. [66 121 101 46 0 0 0 0]

gen: 52 44444444

<<"HiYouBye.">>

并const genLen = 64尝试进行以下 有趣的更改:cap=64更改cap=128(这是预期之外的):


输出:


len=2 cap=8 Hi [72 105 0 0 0 0 0 0]

gen: 50 2222222222222222222222222222222222222222222222222222222222222222

len=3 cap=128   You [89 111 117 58 32 53 48 32 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 10 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]

gen: 51 3333333333333333333333333333333333333333333333333333333333333333

len=4 cap=8 Bye. [66 121 101 46 0 0 0 0]

gen: 52 4444444444444444444444444444444444444444444444444444444444444444

<<"HiYouBye.">>

这些t.Execute(my, my)调用由模板引擎生成func (m *myWriter) Write(p []byte)。plen=3cap=128


在调试文件内第 230 行的第二个代码后/usr/local/go/src/fmt/print.go,它似乎fmt.buffer与length=3, 和cap=128, 在这里:


func Fprint(w io.Writer, a ...interface{}) (n int, err error) {

    p := newPrinter()

    p.doPrint(a)

    n, err = w.Write(p.buf)

    p.free()

    return

}

调用p := newPrinter()此处初始化p.fmt.init(&p.buf):


// newPrinter allocates a new pp struct or grabs a cached one.

func newPrinter() *pp {

    p := ppFree.Get().(*pp)

    p.panicking = false

    p.erroring = false

    p.wrapErrs = false

    p.fmt.init(&p.buf)

    return p

}

获取并返回可用内存,而不将其初始化为零。


查看完整回答
反对 回复 2023-07-26
?
繁星coding

TA贡献1797条经验 获得超4个赞

在 Go 中,表达式s.w.Write(p[:64])可以将切片扩展到其长度之外而不会出现错误(直到切片的容量)。在本例中,提供的缓冲区长度仅为 1,但您将其扩展为 64(如输出的第二行所示)。额外的63字节中的内容是未定义的,它恰好是某些方法的输出fmt

解决方案是检查切片的长度。如果你想让切片万无一失,保证无法看到超出其长度的内容,可以使用切片的三索引语法来设置其容量,例如p = p[::len(p)].


查看完整回答
反对 回复 2023-07-26
?
汪汪一只猫

TA贡献1898条经验 获得超8个赞

如果直接给变量赋值而不是这样做fmt.Sprintln,变量就不会泄漏。

代码: https: //play.golang.org/p/Nz0y_MfDjP1

所以我相信fmt.Sprintln是造成泄漏的原因。该函数调用另一个未导出的函数newPrinter来获取 a ,printer而该函数又维护它自己的池和缓存。我还不够深入,但我的猜测是,您手动创建的缓冲区可能会以某种方式在这里重叠/共享。

(如果我发现其他内容,我会更新答案)


查看完整回答
反对 回复 2023-07-26
  • 3 回答
  • 0 关注
  • 132 浏览
慕课专栏
更多

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信