본문으로 건너뛰기

표준 라이브러리 고수 팁

표준 라이브러리를 올바르게, 그리고 효율적으로 사용하는 실전 노하우를 정리했습니다.

표준 라이브러리 우선 원칙

외부 패키지를 추가하기 전에 표준 라이브러리로 해결할 수 있는지 먼저 확인하세요. Go의 표준 라이브러리는 충분히 강력하며, 의존성을 최소화하면 빌드 속도와 보안 취약점 관리가 쉬워집니다.

// ❌ 불필요한 외부 의존성
import "github.com/pkg/errors" // 단순 래핑에는 불필요

// ✅ 표준 라이브러리로 충분
import (
"errors"
"fmt"
)

func findUser(id int) error {
// errors.New — 새 에러 생성, 추가 컨텍스트 없을 때
if id == 0 {
return errors.New("ID는 0이 될 수 없습니다")
}
// fmt.Errorf + %w — 에러 래핑, 컨텍스트 추가 시
if id < 0 {
return fmt.Errorf("findUser: 잘못된 ID %d: %w", id, ErrInvalidID)
}
return nil
}

var ErrInvalidID = errors.New("유효하지 않은 ID")

fmt.Errorf vs errors.New 선택 기준

package main

import (
"errors"
"fmt"
)

var (
ErrNotFound = errors.New("리소스를 찾을 수 없음")
ErrPermission = errors.New("권한 없음")
)

// errors.New 사용 — 고정된 에러, 패키지 레벨 센티넬 에러
var ErrTimeout = errors.New("타임아웃")

// fmt.Errorf + %w 사용 — 동적 컨텍스트 + 언래핑 가능한 에러
func getResource(id int, userRole string) error {
if userRole != "admin" {
// %w로 래핑: errors.Is(err, ErrPermission)이 true
return fmt.Errorf("getResource(id=%d): %w", id, ErrPermission)
}
if id > 1000 {
return fmt.Errorf("getResource: ID %d %w", id, ErrNotFound)
}
return nil
}

// fmt.Errorf (% w 없이) — 래핑 안 함, 단순 메시지
func validate(v string) error {
if v == "" {
return fmt.Errorf("값이 비어 있습니다: 입력값=%q", v)
}
return nil
}

func main() {
err := getResource(42, "user")
fmt.Println(err) // getResource(id=42): 권한 없음
fmt.Println(errors.Is(err, ErrPermission)) // true — %w로 래핑했으므로
}

slog 레벨별 환경 설정

package main

import (
"log/slog"
"os"
)

func initLogger(env string) *slog.Logger {
switch env {
case "production":
// 프로덕션: JSON 형식, Info 이상만 출력
return slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
// AddSource: true, // 필요 시 소스 위치 포함
}))
case "staging":
// 스테이징: JSON 형식, Debug 포함
return slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
}))
default: // "development"
// 개발: 텍스트 형식, 소스 위치 포함
return slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
AddSource: true,
}))
}
}

func main() {
env := os.Getenv("APP_ENV")
if env == "" {
env = "development"
}

logger := initLogger(env)
slog.SetDefault(logger)

// 환경별로 적절한 형식과 레벨로 출력
slog.Debug("디버그 정보", "env", env)
slog.Info("서버 시작", "port", 8080)
slog.Warn("높은 메모리 사용량", "used_mb", 450)
}

regexp 컴파일된 정규식 재사용

package main

import (
"fmt"
"regexp"
)

// ✅ 패키지 레벨 변수로 한 번만 컴파일 — 재사용
var (
emailRe = regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
phoneRe = regexp.MustCompile(`^(\+82|0)\d{9,10}$`)
slugRe = regexp.MustCompile(`[^a-z0-9]+`)
whitespace = regexp.MustCompile(`\s+`)
)

// ❌ 함수 내부에서 매번 컴파일 — 성능 저하
func validateEmailBad(email string) bool {
re := regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
return re.MatchString(email) // 호출마다 컴파일
}

// ✅ 패키지 레벨 변수 사용
func validateEmail(email string) bool {
return emailRe.MatchString(email)
}

func toSlug(s string) string {
// 소문자 변환 후 영숫자 외 문자를 '-'로 교체
lower := whitespace.ReplaceAllString(s, "-")
return slugRe.ReplaceAllString(lower, "-")
}

func main() {
emails := []string{"user@example.com", "invalid", "test@test.co.kr"}
for _, e := range emails {
fmt.Printf("%s: %v\n", e, validateEmail(e))
}

fmt.Println(toSlug("Hello World! Go 언어"))
}

time.Time 직렬화 함정 — 타임존, UTC 변환

package main

import (
"encoding/json"
"fmt"
"time"
)

type Event struct {
Name string `json:"name"`
// ✅ time.Time은 RFC3339 형식으로 자동 직렬화됨 (타임존 정보 포함)
StartTime time.Time `json:"start_time"`
}

func main() {
loc, _ := time.LoadLocation("Asia/Seoul")
now := time.Now().In(loc)

event := Event{Name: "세미나", StartTime: now}
data, _ := json.Marshal(event)
fmt.Println(string(data))
// {"name":"세미나","start_time":"2024-01-15T10:30:00+09:00"}

// ⚠️ 함정 1: 타임존 없는 파싱 — 로컬이 아닌 UTC로 파싱됨
t1, _ := time.Parse("2006-01-02", "2024-01-15")
fmt.Println("타임존 없이 파싱:", t1.Location()) // UTC

// ✅ 올바른 방법: 타임존 명시
t2, _ := time.ParseInLocation("2006-01-02", "2024-01-15", loc)
fmt.Println("타임존 있이 파싱:", t2.Location()) // Asia/Seoul

// ⚠️ 함정 2: DB에 저장 시 UTC 변환 없이 저장
dbTime := now // 로컬 시간 그대로 저장하면 위험

// ✅ DB 저장 전 항상 UTC로 변환
dbTime = now.UTC()
fmt.Println("DB 저장용 UTC:", dbTime.Format(time.RFC3339))

// ⚠️ 함정 3: 시간 비교 시 타임존 무시
t3 := time.Date(2024, 1, 15, 10, 0, 0, 0, loc)
t4 := time.Date(2024, 1, 15, 1, 0, 0, 0, time.UTC) // 같은 순간
fmt.Println("Equal (같은 순간):", t3.Equal(t4)) // true ✅
fmt.Println("== (타임존 포함):", t3 == t4) // false ⚠️

// ✅ 시간 비교는 항상 Equal() 사용
}

JSON 성능 최적화

package main

import (
"bytes"
"encoding/json"
"fmt"
)

// 성능 팁 1: 버퍼 재사용
var jsonBuf = &bytes.Buffer{}

func marshalReuse(v any) ([]byte, error) {
jsonBuf.Reset()
enc := json.NewEncoder(jsonBuf)
if err := enc.Encode(v); err != nil {
return nil, err
}
// 마지막 개행 문자 제거
b := jsonBuf.Bytes()
if len(b) > 0 && b[len(b)-1] == '\n' {
b = b[:len(b)-1]
}
result := make([]byte, len(b))
copy(result, b)
return result, nil
}

// 성능 팁 2: json.Number로 큰 정수/정밀도 보존
func parseWithPrecision() {
jsonStr := `{"id": 9007199254740993, "price": 1234567890.123456789}`

// ❌ 기본 파싱 — float64 정밀도 손실
var v1 map[string]interface{}
json.Unmarshal([]byte(jsonStr), &v1)
fmt.Printf("float64 id: %v\n", v1["id"]) // 정밀도 손실 가능

// ✅ json.Number 사용 — 원본 문자열 보존
decoder := json.NewDecoder(bytes.NewReader([]byte(jsonStr)))
decoder.UseNumber()
var v2 map[string]interface{}
decoder.Decode(&v2)
id := v2["id"].(json.Number)
fmt.Printf("Number id: %s\n", id.String()) // 정확한 값

// 성능 팁 3: 알려진 구조는 map보다 struct로 파싱 (2~3배 빠름)
}

func main() {
data := map[string]string{"hello": "world"}
b, err := marshalReuse(data)
if err != nil {
fmt.Println("오류:", err)
return
}
fmt.Println(string(b))

parseWithPrecision()
}

reflect 패키지 주의사항

package main

import (
"fmt"
"reflect"
)

type Config struct {
Host string
Port int
TLS bool
}

// ⚠️ reflect는 성능 비용이 큼 — 핫 패스에서 피할 것
func printFields(v any) {
rv := reflect.ValueOf(v)
rt := rv.Type()

for i := 0; i < rt.NumField(); i++ {
field := rt.Field(i)
value := rv.Field(i)
fmt.Printf("%s: %v (타입: %s)\n", field.Name, value.Interface(), field.Type)
}
}

// ✅ 대안: 인터페이스나 명시적 함수 사용
func printConfig(c Config) {
fmt.Printf("Host: %s, Port: %d, TLS: %v\n", c.Host, c.Port, c.TLS)
}

// ✅ reflect가 적합한 경우: 범용 라이브러리, ORM, 직렬화 프레임워크
func deepEqual(a, b any) bool {
return reflect.DeepEqual(a, b)
}

func main() {
cfg := Config{Host: "localhost", Port: 8080, TLS: true}

fmt.Println("=== reflect 사용 ===")
printFields(cfg) // 느리지만 범용적

fmt.Println("=== 직접 사용 ===")
printConfig(cfg) // 빠르고 명확

// DeepEqual — 중첩 구조 비교에 유용
s1 := []int{1, 2, 3}
s2 := []int{1, 2, 3}
s3 := []int{1, 2, 4}
fmt.Println("s1 == s2:", deepEqual(s1, s2)) // true
fmt.Println("s1 == s3:", deepEqual(s1, s3)) // false

// reflect 주의사항:
// 1. reflect.Value.Interface() 호출 시 패닉 가능 (unexported 필드)
// 2. 제네릭 도입 후 많은 용도가 대체됨 (Go 1.18+)
// 3. 컴파일 시 타입 검사 불가 — 런타임 에러 위험
}

핵심 요약

권장 사항
에러 생성고정 메시지 → errors.New, 컨텍스트 추가 → fmt.Errorf + %w
로깅프로덕션 → slog JSON, 개발 → slog 텍스트
정규식패키지 레벨 MustCompile 변수로 재사용
시간 저장DB 저장 전 .UTC() 변환, 비교 시 .Equal() 사용
JSON 파싱알려진 구조 → 구조체, 정밀도 필요 → UseNumber()
reflect범용 라이브러리에서만, 핫 패스에서는 인터페이스나 제네릭 사용