Как долго будет расширяться буфер?

Это стандартный метод Write() в пакете bytes

// Write appends the contents of p to the buffer, growing the buffer as
// needed. The return value n is the length of p; err is always nil. If the
// buffer becomes too large, Write will panic with ErrTooLarge.
func (b *Buffer) Write(p []byte) (n int, err error) {
    b.lastRead = opInvalid
    m, ok := b.tryGrowByReslice(len(p))
    if !ok {
        m = b.grow(len(p))
    }
    return copy(b.buf[m:], p), nil
}

Правильно ли я понимаю, что данный метод будет расширять буфер и сам го не будет оповещать о раздутии буфера пока система не прекратит давать ресурс? А когда система прекратит давать ресурс, то программа просто запаникует и упадет? И какие есть "правильные" способы ограничить раздутие буфера, чтобы паники точно не было?


Ответы (1 шт):

Автор решения: Pak Uula

Короткий ответ

никак нельзя обработать завершение памяти в Go.

Длинный ответ

Документация bytes.Buffer вводит в заблуждение. Когда go runtime получает отказ в выделении памяти, это не паника, а fatal error. Вот что пишет приложение, когда заканчивается память:

runtime: out of memory: cannot allocate 268435456-byte block (272007168 in use)
fatal error: out of memory

goroutine 1 gp=0xc0000061c0 m=9 mp=0xc00005f508 [running]:
runtime.throw({0x4b8291?, 0x8000?})
        /usr/local/go/src/runtime/panic.go:1067 +0x48 fp=0xc00011eb10 sp=0xc00011eae0 pc=0x466dc8
...

В документации на runtime.throw написано

throw triggers a fatal error that dumps a stack trace and exits.

throw - это не паника, а аварийное завершение программы. Это событие нельзя перехватить. Код runtime.throw вызывает функцию runtime.fatalthrow которая внутри себя вызывает exit(2). То есть до паники вообще дело не доходит, рантайм печатает стек и завершает процесс к кодом 2.

Тестовый пример

package main

import (
    "bytes"
    "fmt"
    "io"
    "os"
    "os/signal"
    "strings"
    "syscall"
)

func main() {
    signals := make(chan os.Signal, 1)
    signal.Notify(signals, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT)
    go func() {
        sig := <-signals
        fmt.Println("Received signal:", sig)
        os.Exit(0)
    }()

    buf := bytes.NewBuffer(nil)
    factor := 1.2
    size := 16 * 1024 * 1024
    chunkSize := size
    total := 0
    data := bytes.Repeat([]byte("0123456789abcdef"), chunkSize/16)
    for {
        fmt.Println("Total written:", intWithDelims(uint64(total), "_"))
        n, err := testWrite(buf, data, size-total)
        if err != nil {
            fmt.Println("Error:", err)
            break
        }
        total += n
        size = int(float64(size) * factor)
    }
}

func testWrite(buf io.Writer, data []byte, size int) (int, error) {
    chunkSize := len(data)
    total := 0
    for i := 0; i < size; i += chunkSize {

        n, err := buf.Write(data)
        if err != nil {
            fmt.Println("Error writing to buffer:", err)
            return total, err
        }
        if n < chunkSize {
            fmt.Println("Smaller write")
            return total, fmt.Errorf("smaller write")
        }
        total += n
    }
    return total, nil
}

func intWithDelims(v uint64, delim string) string {
    if delim == "" {
        return fmt.Sprintf("%d", v)
    }
    if v == 0 {
        return "0"
    }
    triads := []string{}
    for v > 0 {
        t := v % 1000
        v = v / 1000
        if v == 0 {
            triads = append(triads, fmt.Sprintf("%d", t))
            break
        } else {
            triads = append(triads, fmt.Sprintf("%03d", t))
        }
    }
    // Reverse triads
    for i, j := 0, len(triads)-1; i < j; i, j = i+1, j-1 {
        triads[i], triads[j] = triads[j], triads[i]
    }
    return strings.Join(triads, delim)
}

Собрать и запустить с ограничением виртуальной памяти 100 мегабайт:

go build -o ./buffer . && ( ulimit -v 1000000 && exec ./buffer )

Результат

Total written: 0
Total written: 16_777_216
Total written: 33_554_432
Total written: 33_554_432
Total written: 33_554_432
Total written: 50_331_648
Total written: 50_331_648
Total written: 50_331_648
Total written: 67_108_864
Total written: 83_886_080
Total written: 100_663_296
Total written: 117_440_512
Total written: 134_217_728
runtime: out of memory: cannot allocate 268435456-byte block (272072704 in use)
fatal error: out of memory

goroutine 1 gp=0xc0000061c0 m=6 mp=0xc00005f508 [running]:
runtime.throw({0x4b8191?, 0x8000?})
        /usr/local/go/src/runtime/panic.go:1067 +0x48 fp=0xc0000acb80 sp=0xc0000acb50 pc=0x466dc8
runtime.(*mcache).allocLarge(0x0?, 0x10000000, 0x1)
        /usr/local/go/src/runtime/mcache.go:236 +0x18b fp=0xc0000acbd0 sp=0xc0000acb80 pc=0x41180b
runtime.mallocgc(0x10000000, 0x0, 0x0)
        /usr/local/go/src/runtime/malloc.go:1177 +0x5d0 fp=0xc0000acc70 sp=0xc0000acbd0 pc=0x4631b0
runtime.growslice(0x0, 0x10?, 0x10?, 0xc0000acd90?, 0x490846?)
        /usr/local/go/src/runtime/slice.go:264 +0x5c9 fp=0xc0000acce0 sp=0xc0000acc70 pc=0x468b89
bytes.growSlice({0xc008282000, 0x8000000, 0xc00001c240?}, 0x4897a0?)
        /usr/local/go/src/bytes/buffer.go:249 +0x93 fp=0xc0000acd70 sp=0xc0000acce0 pc=0x474fb3
bytes.(*Buffer).grow(0xc000110000, 0x1000000)
        /usr/local/go/src/bytes/buffer.go:151 +0x13d fp=0xc0000acda8 sp=0xc0000acd70 pc=0x474cfd
bytes.(*Buffer).Write(0xc000110000, {0xc000180000, 0x1000000, 0x4b2da0?})
        /usr/local/go/src/bytes/buffer.go:179 +0x59 fp=0xc0000acdd8 sp=0xc0000acda8 pc=0x474e59
main.testWrite({0x4dd788, 0xc000110000}, {0xc000180000, 0x1000000, 0x1000000}, 0xea857e)
→ Ссылка