[Go] finalizer
개요
Finalizer는 객체가 가비지 컬렉션되기 직전에 호출되는 함수로, runtime.SetFinalizer를 통해 설정합니다.
주요 특징:
- 실행 비보장: GC 시점에만 실행되며, 프로그램 종료 시 실행 안 될 수 있음
- 디버깅 용도: 리소스 누수 감지, 메모리 관리 검증
- 소멸자 아님: C++/Java의 소멸자와 다름, 정리 보장 불가
- 단일 finalizer: 객체당 하나만 설정 가능
- GC 의존: 가비지 컬렉션 사이클에 의존
- 성능 영향: Finalizer가 있는 객체는 GC 처리가 느림
- 대안 선호: defer, Close 패턴이 더 안전
기본 개념
1. SetFinalizer 기본
package main
import (
"fmt"
"runtime"
"time"
)
type Resource struct {
ID int
}
func main() {
// Finalizer 설정
r := &Resource{ID: 1}
runtime.SetFinalizer(r, func(res *Resource) {
fmt.Printf("Resource %d is being collected\n", res.ID)
})
// r을 nil로 만들어 GC 대상으로
r = nil
// GC 강제 실행
runtime.GC()
time.Sleep(100 * time.Millisecond)
// Output: Resource 1 is being collected
}
2. Finalizer 해제
func main() {
r := &Resource{ID: 2}
// Finalizer 설정
runtime.SetFinalizer(r, func(res *Resource) {
fmt.Println("Finalizer called")
})
// Finalizer 제거 (nil 전달)
runtime.SetFinalizer(r, nil)
r = nil
runtime.GC()
time.Sleep(100 * time.Millisecond)
// Finalizer가 호출되지 않음
}
3. 실행 시점
func main() {
fmt.Println("Start")
for i := 0; i < 5; i++ {
r := &Resource{ID: i}
runtime.SetFinalizer(r, func(res *Resource) {
fmt.Printf("Finalizer %d\n", res.ID)
})
}
fmt.Println("Loop done")
// GC가 실행되지 않으면 finalizer도 실행 안 됨
runtime.GC()
time.Sleep(100 * time.Millisecond)
fmt.Println("End")
// Output 순서 보장 안 됨:
// Start
// Loop done
// Finalizer 3
// Finalizer 1
// Finalizer 4
// ...
// End
}
실행 특성
1. GC 사이클 의존
func demonstrateGC() {
// GC 없이는 finalizer 실행 안 됨
for i := 0; i < 100; i++ {
r := &Resource{ID: i}
runtime.SetFinalizer(r, func(res *Resource) {
fmt.Printf("GC: %d\n", res.ID)
})
}
fmt.Println("Created 100 resources")
// GC가 자동으로 실행될 때까지 finalizer 대기
// 강제 GC
runtime.GC()
time.Sleep(100 * time.Millisecond)
}
2. 프로그램 종료 시
func main() {
r := &Resource{ID: 999}
runtime.SetFinalizer(r, func(res *Resource) {
fmt.Println("This may not be printed")
})
r = nil
// 프로그램 종료 시 finalizer 실행 보장 없음
// GC가 실행되지 않을 수 있음
}
3. 순환 참조
type Node struct {
ID int
Next *Node
}
func main() {
// 순환 참조
n1 := &Node{ID: 1}
n2 := &Node{ID: 2}
n1.Next = n2
n2.Next = n1
runtime.SetFinalizer(n1, func(n *Node) {
fmt.Printf("Node %d collected\n", n.ID)
})
runtime.SetFinalizer(n2, func(n *Node) {
fmt.Printf("Node %d collected\n", n.ID)
})
// 순환 참조 끊기
n1.Next = nil
n2.Next = nil
n1, n2 = nil, nil
runtime.GC()
time.Sleep(100 * time.Millisecond)
}
4. 실행 고루틴
func main() {
r := &Resource{ID: 100}
runtime.SetFinalizer(r, func(res *Resource) {
// Finalizer는 별도 고루틴에서 실행
fmt.Printf("Goroutine: %d\n", res.ID)
// 동기화 필요 시 주의
time.Sleep(10 * time.Millisecond)
})
r = nil
runtime.GC()
time.Sleep(200 * time.Millisecond)
}
메모리 관리
1. 메모리 프로파일링
import (
"fmt"
"runtime"
)
type LargeObject struct {
Data [1024 * 1024]byte // 1MB
ID int
}
func trackMemory() {
var m runtime.MemStats
// 메모리 할당 전
runtime.ReadMemStats(&m)
before := m.Alloc
// 객체 생성 및 finalizer 설정
for i := 0; i < 10; i++ {
obj := &LargeObject{ID: i}
runtime.SetFinalizer(obj, func(o *LargeObject) {
fmt.Printf("Collected: %d\n", o.ID)
})
}
// 메모리 할당 후
runtime.ReadMemStats(&m)
after := m.Alloc
fmt.Printf("Allocated: %d MB\n", (after-before)/(1024*1024))
// GC 실행
runtime.GC()
time.Sleep(100 * time.Millisecond)
// 메모리 해제 후
runtime.ReadMemStats(&m)
fmt.Printf("After GC: %d MB\n", m.Alloc/(1024*1024))
}
2. Finalizer 큐
func demonstrateQueue() {
// Finalizer가 많으면 GC 성능 저하
count := 10000
start := time.Now()
for i := 0; i < count; i++ {
r := &Resource{ID: i}
runtime.SetFinalizer(r, func(res *Resource) {
// 빈 finalizer도 오버헤드 있음
})
}
fmt.Printf("Set %d finalizers: %v\n", count, time.Since(start))
start = time.Now()
runtime.GC()
time.Sleep(500 * time.Millisecond)
fmt.Printf("GC with finalizers: %v\n", time.Since(start))
}
3. 재부활 (Resurrection)
var global *Resource
func main() {
r := &Resource{ID: 1}
runtime.SetFinalizer(r, func(res *Resource) {
fmt.Println("Finalizer called")
// 객체를 전역 변수에 저장 (재부활)
global = res
// 재부활된 객체는 다시 GC 대상이 됨
// 하지만 finalizer는 다시 호출 안 됨
})
r = nil
runtime.GC()
time.Sleep(100 * time.Millisecond)
if global != nil {
fmt.Printf("Resurrected: %d\n", global.ID)
}
}
일반적인 사용 사례
1. 파일 디스크립터 추적
import (
"fmt"
"os"
)
type FileWrapper struct {
file *os.File
name string
}
func NewFileWrapper(name string) (*FileWrapper, error) {
f, err := os.Open(name)
if err != nil {
return nil, err
}
fw := &FileWrapper{file: f, name: name}
// Finalizer로 미닫힘 감지
runtime.SetFinalizer(fw, func(wrapper *FileWrapper) {
if wrapper.file != nil {
fmt.Printf("WARNING: File %s not closed properly\n", wrapper.name)
wrapper.file.Close()
}
})
return fw, nil
}
func (fw *FileWrapper) Close() error {
if fw.file == nil {
return nil
}
err := fw.file.Close()
fw.file = nil
// 정상 닫힘, finalizer 제거
runtime.SetFinalizer(fw, nil)
return err
}
func main() {
// 올바른 사용
fw1, _ := NewFileWrapper("test1.txt")
fw1.Close()
// 잘못된 사용 (Close 호출 안 함)
fw2, _ := NewFileWrapper("test2.txt")
_ = fw2 // 사용 후 Close 없이 버림
runtime.GC()
time.Sleep(100 * time.Millisecond)
// WARNING: File test2.txt not closed properly
}
2. C 라이브러리 리소스
/*
#include <stdlib.h>
typedef struct {
int* data;
int size;
} CArray;
CArray* create_array(int size) {
CArray* arr = (CArray*)malloc(sizeof(CArray));
arr->data = (int*)malloc(size * sizeof(int));
arr->size = size;
return arr;
}
void free_array(CArray* arr) {
free(arr->data);
free(arr);
}
*/
import "C"
import "unsafe"
type CArrayWrapper struct {
carray *C.CArray
}
func NewCArray(size int) *CArrayWrapper {
wrapper := &CArrayWrapper{
carray: C.create_array(C.int(size)),
}
// C 메모리 누수 방지
runtime.SetFinalizer(wrapper, func(w *CArrayWrapper) {
if w.carray != nil {
fmt.Println("WARNING: C array not freed")
C.free_array(w.carray)
}
})
return wrapper
}
func (w *CArrayWrapper) Free() {
if w.carray != nil {
C.free_array(w.carray)
w.carray = nil
runtime.SetFinalizer(w, nil)
}
}
3. 데이터베이스 연결 풀
import "database/sql"
type DBConnection struct {
conn *sql.DB
id string
}
type ConnectionPool struct {
connections map[string]*DBConnection
mu sync.Mutex
}
func (p *ConnectionPool) Get(id string) *DBConnection {
p.mu.Lock()
defer p.mu.Unlock()
conn := &DBConnection{id: id}
// 실제로는 sql.Open() 호출
p.connections[id] = conn
// 반환 안 된 연결 추적
runtime.SetFinalizer(conn, func(c *DBConnection) {
fmt.Printf("WARNING: Connection %s not returned to pool\n", c.id)
})
return conn
}
func (p *ConnectionPool) Return(conn *DBConnection) {
p.mu.Lock()
defer p.mu.Unlock()
delete(p.connections, conn.id)
runtime.SetFinalizer(conn, nil)
}
4. 임시 파일 정리
import (
"io/ioutil"
"os"
"path/filepath"
)
type TempFile struct {
path string
file *os.File
}
func NewTempFile() (*TempFile, error) {
f, err := ioutil.TempFile("", "example-*.txt")
if err != nil {
return nil, err
}
tf := &TempFile{
path: f.Name(),
file: f,
}
// 미정리 임시 파일 감지
runtime.SetFinalizer(tf, func(t *TempFile) {
if t.path != "" {
fmt.Printf("WARNING: Temp file %s not cleaned up\n", t.path)
os.Remove(t.path)
}
})
return tf, nil
}
func (tf *TempFile) Close() error {
if tf.file == nil {
return nil
}
err := tf.file.Close()
if err != nil {
return err
}
// 파일 삭제
os.Remove(tf.path)
tf.path = ""
tf.file = nil
runtime.SetFinalizer(tf, nil)
return nil
}
실전 예제
1. 리소스 누수 탐지기
type ResourceTracker struct {
resources map[string]int
mu sync.Mutex
}
var tracker = &ResourceTracker{
resources: make(map[string]int),
}
func (rt *ResourceTracker) Track(name string, obj interface{}) {
rt.mu.Lock()
rt.resources[name]++
rt.mu.Unlock()
runtime.SetFinalizer(obj, func(o interface{}) {
rt.mu.Lock()
rt.resources[name]--
rt.mu.Unlock()
})
}
func (rt *ResourceTracker) Report() {
rt.mu.Lock()
defer rt.mu.Unlock()
fmt.Println("Resource Leak Report:")
for name, count := range rt.resources {
if count > 0 {
fmt.Printf(" %s: %d leaked\n", name, count)
}
}
}
// 사용 예
type DatabaseConnection struct {
ID int
}
func NewDBConnection(id int) *DatabaseConnection {
conn := &DatabaseConnection{ID: id}
tracker.Track("db_connection", conn)
return conn
}
func main() {
// 일부 연결은 정상 해제
for i := 0; i < 5; i++ {
conn := NewDBConnection(i)
_ = conn
}
// GC 실행
runtime.GC()
time.Sleep(100 * time.Millisecond)
tracker.Report()
// Resource Leak Report:
// db_connection: 5 leaked
}
2. 메모리 풀 관리
type Buffer struct {
data []byte
pool *BufferPool
}
type BufferPool struct {
buffers chan *Buffer
created int
mu sync.Mutex
}
func NewBufferPool(size, capacity int) *BufferPool {
return &BufferPool{
buffers: make(chan *Buffer, capacity),
}
}
func (p *BufferPool) Get(size int) *Buffer {
select {
case buf := <-p.buffers:
return buf
default:
p.mu.Lock()
p.created++
p.mu.Unlock()
buf := &Buffer{
data: make([]byte, size),
pool: p,
}
// 반환 안 된 버퍼 추적
runtime.SetFinalizer(buf, func(b *Buffer) {
fmt.Printf("WARNING: Buffer not returned to pool\n")
})
return buf
}
}
func (p *BufferPool) Put(buf *Buffer) {
runtime.SetFinalizer(buf, nil)
select {
case p.buffers <- buf:
default:
// 풀이 꽉 참, 버퍼 폐기
}
}
func main() {
pool := NewBufferPool(1024, 10)
// 올바른 사용
buf1 := pool.Get(1024)
pool.Put(buf1)
// 잘못된 사용
buf2 := pool.Get(1024)
_ = buf2 // 반환 안 함
runtime.GC()
time.Sleep(100 * time.Millisecond)
}
3. 네트워크 연결 모니터링
import "net"
type MonitoredConn struct {
net.Conn
id string
createdAt time.Time
}
func WrapConn(conn net.Conn, id string) *MonitoredConn {
mc := &MonitoredConn{
Conn: conn,
id: id,
createdAt: time.Now(),
}
runtime.SetFinalizer(mc, func(c *MonitoredConn) {
duration := time.Since(c.createdAt)
fmt.Printf("WARNING: Connection %s not closed (lived %v)\n",
c.id, duration)
c.Conn.Close()
})
return mc
}
func (mc *MonitoredConn) Close() error {
runtime.SetFinalizer(mc, nil)
return mc.Conn.Close()
}
func main() {
// 연결 생성
conn, _ := net.Dial("tcp", "example.com:80")
mc := WrapConn(conn, "conn-1")
// Close 호출 안 함
_ = mc
runtime.GC()
time.Sleep(100 * time.Millisecond)
}
4. 캐시 엔트리 만료
type CacheEntry struct {
Key string
Value interface{}
ExpiresAt time.Time
}
type Cache struct {
entries map[string]*CacheEntry
mu sync.RWMutex
}
func NewCache() *Cache {
return &Cache{
entries: make(map[string]*CacheEntry),
}
}
func (c *Cache) Set(key string, value interface{}, ttl time.Duration) {
entry := &CacheEntry{
Key: key,
Value: value,
ExpiresAt: time.Now().Add(ttl),
}
c.mu.Lock()
c.entries[key] = entry
c.mu.Unlock()
// 만료 시 자동 삭제 (보조 메커니즘)
runtime.SetFinalizer(entry, func(e *CacheEntry) {
c.mu.Lock()
delete(c.entries, e.Key)
c.mu.Unlock()
fmt.Printf("Cache entry %s finalized\n", e.Key)
})
}
func (c *Cache) Get(key string) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
entry, ok := c.entries[key]
if !ok {
return nil, false
}
if time.Now().After(entry.ExpiresAt) {
return nil, false
}
return entry.Value, true
}
5. 작업 추적 시스템
type Task struct {
ID string
StartTime time.Time
finished bool
}
type TaskTracker struct {
tasks map[string]*Task
mu sync.Mutex
}
var taskTracker = &TaskTracker{
tasks: make(map[string]*Task),
}
func StartTask(id string) *Task {
task := &Task{
ID: id,
StartTime: time.Now(),
}
taskTracker.mu.Lock()
taskTracker.tasks[id] = task
taskTracker.mu.Unlock()
// 미완료 작업 경고
runtime.SetFinalizer(task, func(t *Task) {
if !t.finished {
duration := time.Since(t.StartTime)
fmt.Printf("WARNING: Task %s not finished (running %v)\n",
t.ID, duration)
}
})
return task
}
func (t *Task) Finish() {
t.finished = true
taskTracker.mu.Lock()
delete(taskTracker.tasks, t.ID)
taskTracker.mu.Unlock()
runtime.SetFinalizer(t, nil)
}
func main() {
// 완료된 작업
task1 := StartTask("task-1")
time.Sleep(10 * time.Millisecond)
task1.Finish()
// 미완료 작업
task2 := StartTask("task-2")
_ = task2
runtime.GC()
time.Sleep(100 * time.Millisecond)
}
6. 글로벌 리소스 카운터
type ResourceCounter struct {
name string
count *int64
}
var counters sync.Map // map[string]*int64
func TrackResource(name string) *ResourceCounter {
// 카운터 가져오기 또는 생성
val, _ := counters.LoadOrStore(name, new(int64))
counter := val.(*int64)
// 카운트 증가
atomic.AddInt64(counter, 1)
rc := &ResourceCounter{
name: name,
count: counter,
}
// 카운트 감소
runtime.SetFinalizer(rc, func(r *ResourceCounter) {
atomic.AddInt64(r.count, -1)
})
return rc
}
func GetResourceCount(name string) int64 {
val, ok := counters.Load(name)
if !ok {
return 0
}
return atomic.LoadInt64(val.(*int64))
}
func main() {
// 리소스 생성
for i := 0; i < 100; i++ {
_ = TrackResource("api_client")
}
fmt.Printf("Active: %d\n", GetResourceCount("api_client"))
// Active: 100
runtime.GC()
time.Sleep(100 * time.Millisecond)
fmt.Printf("After GC: %d\n", GetResourceCount("api_client"))
// After GC: 0
}
7. 디버그 로깅
type DebugObject struct {
Name string
CreatedAt time.Time
file string
line int
}
func NewDebugObject(name string) *DebugObject {
_, file, line, _ := runtime.Caller(1)
obj := &DebugObject{
Name: name,
CreatedAt: time.Now(),
file: filepath.Base(file),
line: line,
}
if os.Getenv("DEBUG") == "1" {
runtime.SetFinalizer(obj, func(o *DebugObject) {
lifetime := time.Since(o.CreatedAt)
fmt.Printf("[DEBUG] %s finalized (lifetime: %v, created at %s:%d)\n",
o.Name, lifetime, o.file, o.line)
})
}
return obj
}
func main() {
os.Setenv("DEBUG", "1")
obj1 := NewDebugObject("cache-entry")
time.Sleep(50 * time.Millisecond)
obj1 = nil
obj2 := NewDebugObject("temp-buffer")
time.Sleep(100 * time.Millisecond)
obj2 = nil
runtime.GC()
time.Sleep(100 * time.Millisecond)
// [DEBUG] cache-entry finalized (lifetime: 50ms, created at main.go:123)
// [DEBUG] temp-buffer finalized (lifetime: 100ms, created at main.go:126)
}
8. 테스트 헬퍼
type TestResource struct {
t *testing.T
name string
cleaned bool
}
func NewTestResource(t *testing.T, name string) *TestResource {
tr := &TestResource{
t: t,
name: name,
}
runtime.SetFinalizer(tr, func(r *TestResource) {
if !r.cleaned {
r.t.Errorf("Test resource %s not cleaned up", r.name)
}
})
return tr
}
func (tr *TestResource) Cleanup() {
tr.cleaned = true
runtime.SetFinalizer(tr, nil)
}
// 테스트에서 사용
func TestExample(t *testing.T) {
// 정상 케이스
res1 := NewTestResource(t, "resource-1")
defer res1.Cleanup()
// 잘못된 케이스 (Cleanup 호출 안 함)
res2 := NewTestResource(t, "resource-2")
_ = res2
// 테스트 종료 시 finalizer 검사
}
일반적인 실수
1. 리소스 정리를 finalizer에 의존
// ❌ 나쁜 예 (finalizer가 호출 안 될 수 있음)
type File struct {
f *os.File
}
func NewFile(name string) *File {
f, _ := os.Open(name)
file := &File{f: f}
runtime.SetFinalizer(file, func(file *File) {
file.f.Close() // 실행 보장 안 됨
})
return file
}
// ✅ 좋은 예 (명시적 Close + finalizer는 보조)
type File struct {
f *os.File
}
func NewFile(name string) *File {
f, _ := os.Open(name)
file := &File{f: f}
runtime.SetFinalizer(file, func(file *File) {
if file.f != nil {
fmt.Println("WARNING: File not closed")
file.f.Close()
}
})
return file
}
func (f *File) Close() error {
if f.f == nil {
return nil
}
err := f.f.Close()
f.f = nil
runtime.SetFinalizer(f, nil)
return err
}
2. 긴 작업 실행
// ❌ 나쁜 예 (finalizer에서 시간 걸리는 작업)
type Logger struct {
logs []string
}
func NewLogger() *Logger {
logger := &Logger{}
runtime.SetFinalizer(logger, func(l *Logger) {
// 파일 쓰기는 시간이 걸림
f, _ := os.Create("logs.txt")
for _, log := range l.logs {
f.WriteString(log + "\n")
}
f.Close()
})
return logger
}
// ✅ 좋은 예 (명시적 Flush)
type Logger struct {
logs []string
}
func (l *Logger) Flush() error {
f, err := os.Create("logs.txt")
if err != nil {
return err
}
defer f.Close()
for _, log := range l.logs {
f.WriteString(log + "\n")
}
runtime.SetFinalizer(l, nil)
return nil
}
3. 복잡한 로직
// ❌ 나쁜 예 (finalizer에서 복잡한 처리)
type Connection struct {
conn net.Conn
pool *ConnectionPool
}
func NewConnection(pool *ConnectionPool) *Connection {
conn := &Connection{pool: pool}
runtime.SetFinalizer(conn, func(c *Connection) {
// 락, 네트워크 호출 등 복잡한 로직
c.pool.mu.Lock()
c.pool.returnConnection(c)
c.pool.mu.Unlock()
})
return conn
}
// ✅ 좋은 예 (단순 경고만)
func NewConnection(pool *ConnectionPool) *Connection {
conn := &Connection{pool: pool}
runtime.SetFinalizer(conn, func(c *Connection) {
fmt.Println("WARNING: Connection not returned")
})
return conn
}
func (c *Connection) Return() {
c.pool.returnConnection(c)
runtime.SetFinalizer(c, nil)
}
4. Finalizer에서 패닉
// ❌ 나쁜 예 (패닉 가능)
type Data struct {
ptr *int
}
func NewData() *Data {
val := 42
data := &Data{ptr: &val}
runtime.SetFinalizer(data, func(d *Data) {
// ptr이 nil일 수 있음
fmt.Println(*d.ptr) // 패닉 가능
})
return data
}
// ✅ 좋은 예 (nil 체크)
func NewData() *Data {
val := 42
data := &Data{ptr: &val}
runtime.SetFinalizer(data, func(d *Data) {
if d.ptr != nil {
fmt.Println(*d.ptr)
}
})
return data
}
5. 객체 재부활 의존
var resurrected *Resource
// ❌ 나쁜 예 (재부활 객체는 finalizer 다시 안 호출됨)
func main() {
r := &Resource{ID: 1}
runtime.SetFinalizer(r, func(res *Resource) {
resurrected = res // 재부활
// 이 finalizer는 다시 호출되지 않음
})
r = nil
runtime.GC()
time.Sleep(100 * time.Millisecond)
// resurrected를 다시 nil로 만들어도 finalizer 실행 안 됨
resurrected = nil
runtime.GC()
time.Sleep(100 * time.Millisecond)
}
// ✅ 좋은 예 (재부활 피하기)
func main() {
r := &Resource{ID: 1}
runtime.SetFinalizer(r, func(res *Resource) {
// 단순 로깅만
fmt.Printf("Resource %d collected\n", res.ID)
})
}
6. 순서 의존
// ❌ 나쁜 예 (finalizer 순서 의존)
type Parent struct {
children []*Child
}
type Child struct {
data string
}
func main() {
parent := &Parent{}
runtime.SetFinalizer(parent, func(p *Parent) {
for _, child := range p.children {
// child가 이미 수집되었을 수 있음
fmt.Println(child.data) // 위험
}
})
for i := 0; i < 5; i++ {
child := &Child{data: fmt.Sprintf("child-%d", i)}
parent.children = append(parent.children, child)
}
}
// ✅ 좋은 예 (독립적인 finalizer)
func main() {
parent := &Parent{}
runtime.SetFinalizer(parent, func(p *Parent) {
fmt.Printf("Parent with %d children collected\n", len(p.children))
})
}
7. GC 강제 실행
// ❌ 나쁜 예 (프로덕션에서 GC 강제 실행)
func cleanup() {
// 리소스 정리를 위해 GC 호출
runtime.GC() // 성능 문제
time.Sleep(100 * time.Millisecond)
}
// ✅ 좋은 예 (명시적 정리)
type ResourceManager struct {
resources []*Resource
}
func (rm *ResourceManager) Cleanup() {
for _, r := range rm.resources {
r.Close() // 명시적 정리
}
rm.resources = nil
}
베스트 프랙티스
1. 경고 용도로만 사용
// ✅ Finalizer는 경고/디버깅 용도
type Connection struct {
closed bool
}
func NewConnection() *Connection {
conn := &Connection{}
runtime.SetFinalizer(conn, func(c *Connection) {
if !c.closed {
log.Println("WARNING: Connection not closed properly")
}
})
return conn
}
func (c *Connection) Close() {
c.closed = true
runtime.SetFinalizer(c, nil)
}
2. defer와 함께 사용
// ✅ defer로 확실히 정리
func ProcessFile(name string) error {
fw, err := NewFileWrapper(name)
if err != nil {
return err
}
defer fw.Close() // 확실한 정리
// Finalizer는 보조 (Close 누락 감지)
// 파일 처리
return nil
}
3. 조건부 활성화
// ✅ 디버그 모드에서만 finalizer 활성화
var debugMode = os.Getenv("DEBUG") == "1"
func NewResource(name string) *Resource {
r := &Resource{name: name}
if debugMode {
runtime.SetFinalizer(r, func(res *Resource) {
fmt.Printf("DEBUG: %s not cleaned\n", res.name)
})
}
return r
}
4. 통계 수집
// ✅ Finalizer로 통계 수집
var stats struct {
created int64
finalized int64
}
type Object struct {
id int
}
func NewObject(id int) *Object {
atomic.AddInt64(&stats.created, 1)
obj := &Object{id: id}
runtime.SetFinalizer(obj, func(o *Object) {
atomic.AddInt64(&stats.finalized, 1)
})
return obj
}
func GetStats() (created, finalized int64) {
return atomic.LoadInt64(&stats.created),
atomic.LoadInt64(&stats.finalized)
}
5. 짧고 단순하게
// ✅ Finalizer는 짧고 단순하게
type Resource struct {
id string
}
func NewResource(id string) *Resource {
r := &Resource{id: id}
runtime.SetFinalizer(r, func(res *Resource) {
// 단순 로깅만
log.Printf("Resource %s finalized", res.id)
})
return r
}
6. 테스트에서 활용
// ✅ 테스트에서 리소스 누수 검증
func TestResourceLeak(t *testing.T) {
var leaked bool
func() {
r := &Resource{}
runtime.SetFinalizer(r, func(*Resource) {
leaked = true
})
// Close 호출 안 함
}()
runtime.GC()
time.Sleep(100 * time.Millisecond)
if !leaked {
t.Error("Resource should have been collected")
}
}
7. 문서화
// ✅ Finalizer 동작 문서화
// NewDBConn creates a new database connection.
// The connection MUST be closed by calling Close().
// A finalizer is set to detect unclosed connections in debug builds.
func NewDBConn(dsn string) (*DBConn, error) {
conn := &DBConn{}
if debugMode {
runtime.SetFinalizer(conn, func(c *DBConn) {
log.Println("WARNING: DB connection not closed")
})
}
return conn, nil
}
8. 정리 함수 제공
// ✅ 명시적 정리 함수 항상 제공
type ResourceManager struct {
resources []*Resource
}
func (rm *ResourceManager) Add(r *Resource) {
rm.resources = append(rm.resources, r)
runtime.SetFinalizer(r, func(res *Resource) {
fmt.Println("WARNING: Resource not removed from manager")
})
}
func (rm *ResourceManager) Remove(r *Resource) {
// 명시적 제거
for i, res := range rm.resources {
if res == r {
rm.resources = append(rm.resources[:i], rm.resources[i+1:]...)
break
}
}
runtime.SetFinalizer(r, nil)
}
func (rm *ResourceManager) Close() {
// 모든 리소스 정리
for _, r := range rm.resources {
runtime.SetFinalizer(r, nil)
}
rm.resources = nil
}
정리
- 기본: SetFinalizer로 GC 전 호출 함수 설정
- 실행: GC 사이클 의존, 프로그램 종료 시 보장 없음
- 용도: 디버깅, 경고, 통계, 리소스 누수 감지
- 금지: 리소스 정리 의존, 긴 작업, 복잡한 로직
- 특성: 순서 보장 없음, 재부활 시 재호출 없음, 별도 고루틴
- 실전: 파일 추적, C 리소스, 연결 풀, 임시 파일, 작업 추적
- 실수: 정리 의존, 긴 작업, 복잡한 로직, 패닉, 재부활, 순서 의존, GC 강제
- 베스트: 경고 용도, defer 함께, 조건부, 짧고 단순, 테스트, 문서화, 정리 함수