본문으로 건너뛰기

Go sync.Pool·버퍼 재사용 — Put 이후엔 Bytes()도 남의 것이다

이 글의 순서8개 절

앞 글에서 GOGC와 GOMEMLIMIT을 바꾸는 동안 할당 누계 256MiB는 그대로였다. 이 숫자를 줄이는 방법 중 하나가 짧게 쓰고 버리는 버퍼를 sync.Pool로 다시 쓰는 것이다. 벤치마크에서는 할당이 0번이 됐는데, 반납한 버퍼의 Bytes()를 계속 읽었더니 user=alice가 user=bobce로 바뀌어 있었다. Put은 그 버퍼를 쓸 권한이 끝나는 시점이고, 미리 받아 둔 슬라이스도 거기서 같이 끝난다.

sync.Pool은 보관을 약속하지 않는다

먼저 풀이 무엇을 약속하지 않는지부터 잡고 가자. 문서의 첫 단락에 이렇게 적혀 있다.

풀에 넣은 항목은 언제든 알림 없이 자동으로 제거될 수 있다.

sync.Pool 문서

Get도 이전에 넣은 값을 돌려준다고 약속하지 않는다. 풀이 비어 있으면 New가 새 값을 만들어 줄 뿐이니, 풀에 값이 하나도 없는 순간에도 프로그램은 올바르게 동작해야 한다.

풀은 꺼내 쓰고 돌려놓는 상자다보관은 약속하지 않는다. GC가 돌면 비워진다한 번 빌려 쓰는 순서1Get()비어 있으면 New()가 만든다2Reset()이전 내용을 비운다3encode(b, …)이 호출만 쓴다4Put(b)다음 Get이 가져갈 수 있다GC가 돌 때마다 풀Put한 곳 GC victimGC 1번 뒤 GC 버림GC 2번 뒤victim에 있는 동안은 Get이 꺼내 쓴다두 번째 GC까지 남으면 버리고 New가 만든다
아래 칸에서 GC를 한 번 거친 버퍼는 victim에 남아 있다가 두 번째 GC에서 버려진다

Go 1.13부터는 GC가 풀을 한 번에 비우지 않는다. 풀에 있던 것을 victim으로 옮겨 한 사이클 더 두고, 다음 GC에서 버린다1. 그래도 언젠가 비워진다는 성질은 그대로다.

🧪 직접 해보기

GC를 두 번 거치면 같은 버퍼가 사라진다

정말 두 번째 GC에서 사라지는지 돌려 봤다. 같은 버퍼를 돌려놓고 GC를 0번, 1번, 2번 부른 뒤 다시 꺼낸다.

victim/main.go · 전체 실행 코드
package main
import (
"bytes"
"fmt"
"runtime"
"sync"
)
func main() {
runtime.GOMAXPROCS(1) // P가 하나여야 매번 같은 결과가 나온다
made := 0
pool := sync.Pool{New: func() any { made++; return new(bytes.Buffer) }}
b := pool.Get().(*bytes.Buffer)
for gcs := 0; gcs <= 2; gcs++ {
pool.Put(b)
for i := 0; i < gcs; i++ {
runtime.GC()
}
got := pool.Get().(*bytes.Buffer)
fmt.Printf("GC %d번 뒤: 같은 버퍼=%t, New %d번\n", gcs, got == b, made)
b = got
}
}
실행 결과 (darwin/arm64, Go 1.24.7)
$ go run .
GC 0번 뒤: 같은 버퍼=true, New 1번
GC 1번 뒤: 같은 버퍼=true, New 1번
GC 2번 뒤: 같은 버퍼=false, New 2번

세 번째 줄에서 New가 한 번 더 불렸다. GC 한 번은 버텼고, 두 번째 GC에서 버려졌다. 다섯 번 돌려도, Go 1.27.0으로 바꿔 돌려도 출력은 한 글자도 다르지 않았다.

runtime.GOMAXPROCS(1)은 결과를 고정하려고 넣었다. 풀은 P마다 전용 칸을 두는데, 다른 P의 전용 칸은 꺼내 오지 못한다. P가 여럿이면 고루틴이 다른 P로 옮겨 가는 순간 방금 넣은 버퍼를 못 찾고 New를 부를 수 있다.

그러면 풀은 어디에 맞을까. 이름이 비슷한 구조가 많아서 헷갈리기 쉬운데, 필요한 것이 무엇인지로 갈린다.

필요한 것 맞는 구조 이유
임시 버퍼 재사용 sync.Pool 사라져도 New가 만든다
키와 만료가 있는 캐시 캐시 언제 사라질지 모른다
연결 수 제한 연결 풀 버릴 때 Close를 못 한다
동시 작업 수 제한 워커 풀, 세마포어 빌려 간 수를 세지 않는다

이 글에서는 버려져도 GC가 치우고 다시 만들기 쉬운 bytes.Buffer를 넣는다.

같은 레코드를 세 가지로 써 봤다

출력은 data=, 입력 바이트, 줄바꿈이다. 세 함수는 출력과 에러가 같고, 버퍼를 마련하는 방법만 다르다.

🧪 직접 해보기

1KiB를 쓰는 동안 풀은 할당이 0번이었다

reuse.go · 전체 코드
package reuse
import (
"bytes"
"io"
"sync"
)
const maxRetainedCapacity = 64 << 10
var buffers = sync.Pool{
New: func() any { return new(bytes.Buffer) },
}
func encode(b *bytes.Buffer, payload []byte) {
b.WriteString("data=")
b.Write(payload)
b.WriteByte('\n')
}
// 매번 새 버퍼를 쓰는 기준 구현.
func WriteFresh(w io.Writer, payload []byte) error {
var b bytes.Buffer
encode(&b, payload)
_, err := b.WriteTo(w)
return err
}
// 필요한 용량을 미리 확보하는 개선. 풀은 쓰지 않는다.
func WriteGrown(w io.Writer, payload []byte) error {
var b bytes.Buffer
b.Grow(len(payload) + 6)
encode(&b, payload)
_, err := b.WriteTo(w)
return err
}
// 풀에서 빌려 쓰고 반납한다.
func WritePooled(w io.Writer, payload []byte) error {
b := buffers.Get().(*bytes.Buffer)
b.Reset()
defer func() {
if b.Cap() <= maxRetainedCapacity {
b.Reset()
buffers.Put(b)
}
}()
encode(b, payload)
_, err := b.WriteTo(w)
return err
}

풀에서 받은 버퍼는 한 호출만 쓴다. WriteTo로 결과를 보낸 뒤 지연 함수에서 비우고 돌려준다. 쓰기가 실패해도 지연 함수는 실행되니, 다음 사용에 이전 내용이 넘어가지 않는다.

1KiB 입력을 io.Discard로 · 3회
$ go test -run '^$' -bench . -benchmem -benchtime=200ms -count=3 reuse.go reuse_test.go
BenchmarkFresh-2 576073 434.8 ns/op 1216 B/op 2 allocs/op
BenchmarkFresh-2 584186 378.3 ns/op 1216 B/op 2 allocs/op
BenchmarkFresh-2 533616 436.9 ns/op 1216 B/op 2 allocs/op
BenchmarkGrown-2 569814 387.4 ns/op 1152 B/op 1 allocs/op
BenchmarkGrown-2 570428 387.8 ns/op 1152 B/op 1 allocs/op
BenchmarkGrown-2 606288 386.5 ns/op 1152 B/op 1 allocs/op
BenchmarkPooled-2 6251518 35.30 ns/op 0 B/op 0 allocs/op
BenchmarkPooled-2 7291812 34.32 ns/op 0 B/op 0 allocs/op
BenchmarkPooled-2 7145097 34.63 ns/op 0 B/op 0 allocs/op

Go 1.24.7, linux/amd64 2코어에서 잰 값이다. 1KiB를 io.Discard로 보내는 실험이라 네트워크 처리량과는 거리가 있다.

새 버퍼 쪽 할당이 2번인 게 눈에 띈다. bytes.Buffer는 첫 쓰기가 작으면 64바이트만 잡는다. data=를 쓸 때 64바이트를 잡고, 1KiB 본문을 붙일 때 1152바이트를 다시 잡아서 64 + 1152 = 1216이 B/op에 찍혔다. 풀 쪽은 반복하는 동안 이미 커진 버퍼를 다시 써서 0이 나왔다. 처음 New로 만들 때 생기는 할당까지 없다는 뜻은 아니다.

이 맥(Apple M5 10코어, darwin/arm64)에서도 같은 명령을 Go 1.24.7로 세 번 돌렸다. B/op와 allocs/op는 원문과 똑같았고, ns/op는 하드웨어 차이로 모두 작게 나왔다.

1KiB를 한 번 쓰는 데 걸린 시간 (세 번 중 가운데 값)
  • 매번 새 버퍼128.5ns
  • 미리 Grow119.1ns
  • 풀로 재사용22.8ns

darwin/arm64 Apple M5 · Go 1.24.7 · io.Discard로 보냄

이 숫자를 “sync.Pool을 쓰면 몇 배 빨라진다”로 가져가지는 않는다. io.Discard는 받은 바이트를 그냥 버리니, 인코딩과 임시 버퍼 비용만 남긴 조건이다. 실제 파일이나 네트워크가 끼면 전체 요청에서 이 절약이 차지하는 비율은 작아진다.

Put 뒤에 Bytes()를 읽으면 무엇이 보이나

다음 요청이 같은 배열에 쓴 바이트가 보인다. b.Bytes()는 버퍼 내용의 복사본이 아니라 내부 배열을 가리키는 슬라이스다. 버퍼를 풀에 돌려놓으면 다른 요청이 그 버퍼를 꺼내 Reset하고 다시 쓰는데, 들고 있던 슬라이스로 읽으면 그 내용이 섞여 나온다.

bytes.Buffer 문서도 Bytes()가 돌려준 슬라이스는 다음에 버퍼를 고치기 전까지만 유효하다고 적어 둔다. 풀을 쓰면 그 다음 수정을 다른 요청이 한다.

Get과 Put 사이에만 내 버퍼다Put 뒤에는 미리 받아 둔 슬라이스도 읽으면 안 된다 요청 AA가 쓴다Get()Put() 요청 BB가 쓴다Get()Put() A의 b.Bytes() 읽으면 B가 쓴 내용다음 Get이 같은 버퍼를 준다는 보장도 없다
빨간 구간이 Put 뒤에도 A가 슬라이스를 들고 있는 시간이다

Put을 부르면 버퍼 메서드뿐 아니라 미리 받아 둔 슬라이스로 읽는 것도 거기서 끝내야 한다. 말로 들으면 당연한데, 코드에서는 잘 안 보인다.

🧪 직접 해보기

반납한 뒤에 읽었더니 user=bobce가 나왔다

encodeBad는 슬라이스를 돌려준 뒤 버퍼를 반납하고, encodeGood은 반납하기 전에 복사본을 만든다.

own/main.go · 반납한 뒤에 읽으면
package main
import (
"bytes"
"fmt"
"sync"
)
var pool = sync.Pool{New: func() any { return new(bytes.Buffer) }}
// 나쁜 예: 내부 배열을 가리키는 슬라이스를 돌려주고 버퍼를 반납한다.
func encodeBad(name string) []byte {
b := pool.Get().(*bytes.Buffer)
b.Reset()
b.WriteString("user=")
b.WriteString(name)
out := b.Bytes() // 복사본이 아니라 내부 배열을 보는 슬라이스다
pool.Put(b) // 여기서 사용 권한이 끝난다
return out
}
// 좋은 예: 반납 전에 독립된 복사본을 만든다.
func encodeGood(name string) []byte {
b := pool.Get().(*bytes.Buffer)
b.Reset()
b.WriteString("user=")
b.WriteString(name)
out := append([]byte(nil), b.Bytes()...)
pool.Put(b)
return out
}
func main() {
bad := encodeBad("alice")
fmt.Printf("반납 직후 : %s\n", bad)
_ = encodeBad("bob")
fmt.Printf("다른 요청 뒤 : %s\n", bad)
good := encodeGood("alice")
_ = encodeGood("bob")
fmt.Printf("복사본 : %s\n", good)
// Reset 은 용량을 유지한다 — Len 이 아니라 Cap 으로 봐야 하는 이유
var buf bytes.Buffer
buf.Write(make([]byte, 128<<10))
fmt.Printf("\n큰 쓰기 직후 : Len=%d Cap=%d\n", buf.Len(), buf.Cap())
buf.Reset()
fmt.Printf("Reset 이후 : Len=%d Cap=%d\n", buf.Len(), buf.Cap())
}
실행 결과
$ go run main.go
반납 직후 : user=alice
다른 요청 뒤 : user=bobce
복사본 : user=alice
큰 쓰기 직후 : Len=131072 Cap=131072
Reset 이후 : Len=0 Cap=131072

두 번째 줄을 보자. bob이 alice의 앞 세 글자 자리를 덮어썼고, 꼬리 두 글자 ce는 그대로 남았다.

같은 배열을 다시 읽으면 user=bobcebad는 길이 10인 슬라이스 그대로다A가 쓴 직후 · bad := b.Bytes() u s e r = a l i c eB가 Reset 뒤 user=bob을 쓴다 · 길이 8 u s e r = b o b c eA가 bad를 다시 읽는다 · 길이 10 u s e r = b o b c eB가 쓴 칸alice의 꼬리
주황 세 칸은 B가 덮어쓴 자리이고, 파란 두 칸은 alice가 남긴 꼬리다

값이 통째로 바뀌었다면 금방 눈치챘을 것이다. 이렇게 절반만 섞이면 로그에서도 한참 지나서야 보인다. 나는 다섯 번 돌려 다섯 번 모두 user=bobce를 봤다.

그렇다고 다음 Get이 방금 반납한 버퍼를 준다는 보장은 없다. Get 문서는 Put에 넣은 값과 Get이 돌려주는 값 사이에 어떤 관계도 가정하지 말라고 한다. 타이밍에 따라 다른 버퍼가 나와 겉보기에 멀쩡한 결과가 나올 수도 있다. 들쭉날쭉하게 터지는 버그는 매번 터지는 버그보다 고치기 어렵다.

결과 바이트를 함수 밖에서 계속 써야 한다면 방법은 둘이다. 반납하기 전에 복사본을 만들거나, 받은 쪽이 다 쓰고 반납하도록 소유권 약속을 바꾸는 것이다. 복사를 고르면 복사 비용을 더하고도 풀을 쓰는 이득이 남는지 다시 잰다.

하나 더 짚어 두자. Get과 Put을 여러 고루틴이 동시에 불러도 된다는 건 풀 자체의 이야기다. 꺼낸 버퍼를 여러 고루틴이 함께 고쳐도 된다는 뜻은 아니다. 같은 버퍼를 두 번 Put하거나 반납한 뒤에 계속 쓰는 것도 풀이 막아 주지 않는다. 한 번 쓰기 시작한 Pool 값은 복사해서도 안 된다.

Reset은 길이만 0으로 만든다

위 출력의 마지막 두 줄을 보자. 128KiB를 쓴 뒤 Reset을 부르자 Len은 0이 됐는데 Cap은 131072 그대로였다. Reset은 길이와 읽기 위치만 되돌리고, 다음 쓰기에 다시 쓸 내부 배열은 그대로 둔다.

Reset은 Len만 0으로 만든다그래서 돌려놓을지는 Cap으로 정한다쓴 직후Len=131072 Cap=131072 Reset() 이후Len=0 Cap=131072Put 하기 전에 b.Cap()을 본다Cap ≤ 64KiB풀에 돌려놓는다Cap > 64KiB놓아 둔다 · GC가 회수Reset 뒤 Len은 늘 0이다
두 번째 막대는 Len이 0인데, 테두리로 그린 Cap은 그대로다

앞에서 본 할당 0번이 여기서 나왔다. 같은 성질 때문에 한 번 크게 커진 버퍼는 평소 요청보다 큰 채로 풀에 남는다. 그래서 돌려놓을지는 Cap으로 정한다. Len은 Reset 뒤에 늘 0이라 기준이 되지 못한다. WritePooled가 b.Cap() <= maxRetainedCapacity를 검사한 이유다.

64KiB는 이 실험에서 정한 버퍼 한 개의 보관 기준이다. 어디서나 맞는 값도 아니고, 풀 전체의 메모리 상한도 아니다. 기준을 넘은 버퍼는 Put하지 않고 참조를 놓을 뿐이라, OS에 그만큼이 바로 돌아가지도 않는다. 버퍼마다 상한을 둬도 빌려 간 버퍼 수나 풀 전체 보관량은 정해지지 않는다. 정확한 메모리 예산이 필요하면 따로 관리하는 구조를 둔다.

풀을 넣으면 성능보다 정확성이 먼저 깨진다

그래서 테스트부터 짰다. 빈 입력, 일반 입력, 보관 기준보다 큰 입력의 출력을 세 함수끼리 비교하고, 쓰기 에러 뒤의 재사용과 서로 다른 Writer를 쓰는 동시 호출을 검사한다.

🧪 직접 해보기

에러 경로와 동시 호출 테스트는 -race에서도 통과했다

reuse_test.go · 테스트와 벤치마크
package reuse
import (
"bytes"
"errors"
"io"
"testing"
)
var errWrite = errors.New("쓰기 실패")
type failingWriter struct{}
func (failingWriter) Write(p []byte) (int, error) { return 0, errWrite }
func TestOutput(t *testing.T) {
for _, size := range []int{0, 32, 128 << 10} {
payload := bytes.Repeat([]byte("x"), size)
var fresh, grown, pooled bytes.Buffer
if err := WriteFresh(&fresh, payload); err != nil {
t.Fatal(err)
}
if err := WriteGrown(&grown, payload); err != nil {
t.Fatal(err)
}
if err := WritePooled(&pooled, payload); err != nil {
t.Fatal(err)
}
want := "data=" + string(payload) + "\n"
if fresh.String() != want || grown.String() != want || pooled.String() != want {
t.Fatalf("크기 %d에서 출력이 다르다", size)
}
}
}
func TestErrorThenReuse(t *testing.T) {
if err := WritePooled(failingWriter{}, []byte("이전 값")); !errors.Is(err, errWrite) {
t.Fatalf("쓰기 에러가 보존되지 않았다: %v", err)
}
var out bytes.Buffer
if err := WritePooled(&out, []byte("다음 값")); err != nil {
t.Fatal(err)
}
if out.String() != "data=다음 값\n" {
t.Fatalf("출력이 다르다: %q", out.String())
}
}
func TestConcurrentWrites(t *testing.T) {
for i := 0; i < 16; i++ {
t.Run("독립된 출력", func(t *testing.T) {
t.Parallel()
for j := 0; j < 100; j++ {
var out bytes.Buffer
if err := WritePooled(&out, []byte("hello")); err != nil {
t.Fatal(err)
}
if out.String() != "data=hello\n" {
t.Fatalf("출력이 섞였다: %q", out.String())
}
}
})
}
}
func benchLoop(b *testing.B, fn func(io.Writer, []byte) error) {
payload := bytes.Repeat([]byte("x"), 1024)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if err := fn(io.Discard, payload); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkFresh(b *testing.B) { benchLoop(b, WriteFresh) }
func BenchmarkGrown(b *testing.B) { benchLoop(b, WriteGrown) }
func BenchmarkPooled(b *testing.B) { benchLoop(b, WritePooled) }
레이스 검사
$ go test -race reuse.go reuse_test.go
ok command-line-arguments 1.018s

TestErrorThenReuse는 쓰기가 실패한 다음 호출의 출력이 온전한지 본다. 지연 함수에서 비우고 돌려주니 이전 값이 새어 나오지 않았다. 다음 Get이 같은 버퍼를 준다고 가정하지 않는 테스트라서, 어느 버퍼가 나오든 출력이 맞아야 통과한다.

동시성 테스트는 호출마다 출력 버퍼를 따로 만들었다. 같은 Writer를 여러 고루틴이 함께 쓸 때 필요한 동기화까지 WritePooled가 해 주지는 않는다.

레이스 검사를 통과했다는 건 실행한 그 경로에서 탐지기가 충돌을 찾지 못했다는 뜻이다. 앞 절의 반납 후 읽기는 고루틴 하나 안에서 일어나는 규칙 위반이라 레이스가 아니고, 탐지기가 잡지도 못한다. 이 맥(darwin/arm64, Go 1.24.7)에서 own/main.go를 -race로 빌드해 40번 돌려 봤다.

-race 빌드로 40번 실행
$ go build -race -o own-race . && for i in $(seq 40); do ./own-race | sed -n 2p; done | sort | uniq -c
12 다른 요청 뒤 : user=alice
28 다른 요청 뒤 : user=bobce

경고는 없었다. 오히려 40번 중 12번은 user=alice가 나와 버그가 가려졌다. sync/pool.go를 보면 레이스 검사로 빌드했을 때 Put이 넷 중 하나를 일부러 버린다. 버려진 버퍼 대신 New가 새 버퍼를 만들어 주니, bob이 alice의 배열을 덮어쓸 일이 없다. 그래서 -race 빌드로만 확인하면 이 버그는 넷 중 하나꼴로 숨는다.

풀 없이 Grow만 해도 할당이 준다

벤치마크에 WriteGrown을 끼워 둔 이유가 여기 있다. 결과 길이를 미리 알 수 있으면 필요한 용량을 한 번에 잡을 수 있다. encode 앞에 b.Grow(len(payload) + 6) 한 줄을 넣은 게 전부다.

구현 B/op · allocs/op 대가
매번 새 버퍼 1216 · 2 없음(기준)
미리 Grow 1152 · 1 길이를 알아야 한다
풀로 재사용 0 · 0 반납 규칙, Cap 검사, 복사

원문 측정에서는 할당이 2번에서 1번으로 줄었는데, ns/op는 378.3–436.9와 386.5–387.8로 범위가 겹쳤다. 이 맥에서는 128.5ns에서 119.1ns로 7%쯤 줄었다. 할당이 절반으로 줄었어도 시간은 절반으로 줄지 않았다. 이번에는 풀 쪽 이득이 훨씬 컸지만, 그 결론은 1KiB 고정 입력을 io.Discard로 보낸다는 조건에 붙어 있다.

입력 크기가 들쭉날쭉하거나 동시 요청이 많다면 이 결과로는 부족하다. 작은 요청 사이에 큰 요청을 섞고, GC가 여러 번 도는 긴 실행에서 할당과 남는 메모리를 같이 본다. 풀은 언제든 비워질 수 있으니, New 경로만 타는 상황에서도 기능이 맞고 비용을 감당할 수 있는지 확인한다.

풀의 범위도 정해 둔다. 요청마다 새 풀을 만들면 호출 사이에 다시 쓸 기회가 없다. 반대로 모든 종류와 크기를 풀 하나에 섞으면 무엇이 들어 있고 어떤 상태로 나오는지가 흐려진다. 예제처럼 임시 객체 한 종류를 분명한 반납 규칙으로 관리하는 데서 시작한다.

풀을 들이기 전에 보는 순서위 단계에서 해결되면 아래로 내려가지 않는다1할당이 비용인지 잰다-benchmem과 pprof로 본다2알고리즘을 바꿀 수 있나만드는 횟수 자체를 줄일 수 있는지 본다3결과 길이를 알면 Grow할당 2번 → 1번, 반납 규칙은 없다4그래도 비용이면 sync.Pool할당 0번, 대신 반납 규칙과 Cap 검사
네 번째 칸에서만 할당이 0번이 되고, 반납 규칙이 같이 따라온다

정리

  • sync.Pool은 다시 만들어도 되는 임시 객체를 다시 쓰는 상자다. 보관도 개수도 약속하지 않고, 넣어 둔 버퍼는 GC를 두 번 거치면 버려졌다.
  • Put을 부르면 그 버퍼를 쓸 권한이 끝난다. Bytes()로 받아 둔 슬라이스도 같이 끝나니, 밖으로 내보낼 값은 반납 전에 복사한다.
  • Reset은 Len만 0으로 만들고 Cap은 그대로 둔다. 할당 0번도, 큰 버퍼가 남는 것도 여기서 나온다. 돌려놓을지는 Cap으로 정한다.
  • 반납 뒤 읽기는 레이스가 아니라서 -race가 잡지 못한다. 레이스 빌드에서는 오히려 넷 중 하나꼴로 가려졌다.
  • 결과 길이를 알면 Grow만으로 할당이 2번에서 1번으로 줄었다. 시간은 그만큼 줄지 않았다.

이 글을 쓰면서 제일 오래 붙잡고 있던 건 벤치마크 숫자보다 user=bobce 한 줄이었다. 풀로 할당은 0번이 됐지만, 그만큼 버퍼를 붙잡는 시간이 길어졌고 반납 시점을 지키는 일이 코드에 새로 생겼다.

참고 자료

각주

  1. Go 1.13 릴리스 노트는 풀이 GC마다 전부 다시 채워질 필요가 없어졌고 일부 객체를 남긴다고만 적었다. 한 사이클 동안 victim에 두는 방식은 sync/pool.go의 poolCleanup에 있다. ↩

GitHub에서 고치기
esc

최근 글

↑↓ 이동↵ 열기검색 페이지에서 보기 ›