fmt · log · slog 패키지 — 출력과 로깅의 모든 것
Go 표준 라이브러리에서 가장 자주 사용하는 패키지 세 가지가 바로 fmt, log, slog입니다. fmt는 포맷된 입출력을, log는 간단한 로깅을, slog(Go 1.21+)는 구조화된 고성능 로깅을 담당합니다.
fmt 패키지 — 포맷 입출력
출력 함수
fmt 패키지는 세 가지 출력 대상을 지원합니다.
package main
import (
"fmt"
"os"
)
func main() {
// Print 계열 — 표준 출력(stdout)에 쓰기
fmt.Print("줄바꿈 없음") // 줄바꿈 없음
fmt.Println("줄바꿈 포함") // 자동 줄바꿈
fmt.Printf("이름: %s, 나이: %d\n", "Alice", 30) // 포맷 지정
// Sprint 계열 — 문자열로 반환
s1 := fmt.Sprint("Hello", " ", "World") // "Hello World"
s2 := fmt.Sprintln("Hello", "World") // "Hello World\n"
s3 := fmt.Sprintf("Pi = %.4f", 3.14159265) // "Pi = 3.1416"
fmt.Println(s1, s2, s3)
// Fprint 계열 — io.Writer에 쓰기
fmt.Fprint(os.Stderr, "에러 메시지\n")
fmt.Fprintln(os.Stdout, "표준 출력")
fmt.Fprintf(os.Stderr, "에러 코드: %d\n", 404)
}
포맷팅 동사(Verbs) 완전 정복
Go의 포맷 동사는 % 뒤에 문자를 붙여 출력 형식을 지정합니다.
package main
import "fmt"
type Person struct {
Name string
Age int
}
func main() {
p := Person{"Alice", 30}
num := 255
pi := 3.14159265358979
// 범용 동사
fmt.Printf("%v\n", p) // {Alice 30} — 기본 형식
fmt.Printf("%+v\n", p) // {Name:Alice Age:30} — 필드 이름 포함
fmt.Printf("%#v\n", p) // main.Person{Name:"Alice", Age:30} — Go 구문 표현
fmt.Printf("%T\n", p) // main.Person — 타입 출력
// 정수 동사
fmt.Printf("%d\n", num) // 255 — 10진수
fmt.Printf("%b\n", num) // 11111111 — 2진수
fmt.Printf("%o\n", num) // 377 — 8진수
fmt.Printf("%x\n", num) // ff — 16진수 소문자
fmt.Printf("%X\n", num) // FF — 16진수 대문자
fmt.Printf("%08d\n", num) // 00000255 — 너비 8, 0 패딩
fmt.Printf("%-8d|\n", num) // 255 | — 왼쪽 정렬
// 부동소수점 동사
fmt.Printf("%f\n", pi) // 3.141593 — 기본 소수점
fmt.Printf("%.2f\n", pi) // 3.14 — 소수점 2자리
fmt.Printf("%e\n", pi) // 3.141593e+00 — 지수 표기
fmt.Printf("%g\n", pi) // 3.14159265358979 — 더 짧은 표기
// 문자열 동사
fmt.Printf("%s\n", "Hello") // Hello — 문자열
fmt.Printf("%q\n", "Hello") // "Hello" — 따옴표 포함
fmt.Printf("%10s\n", "Go") // Go — 너비 10, 오른쪽 정렬
fmt.Printf("%-10s|\n", "Go") // Go | — 왼쪽 정렬
// 포인터 동사
x := 42
fmt.Printf("%p\n", &x) // 0xc0000b4008 — 포인터 주소
// 불리언 동사
fmt.Printf("%t\n", true) // true
}
표준 입력 읽기 — Scan 계열
package main
import "fmt"
func main() {
var name string
var age int
// Scan — 공백으로 구분된 입력 읽기
fmt.Print("이름과 나이를 입력하세요: ")
n, err := fmt.Scan(&name, &age)
if err != nil {
fmt.Println("입력 오류:", err)
return
}
fmt.Printf("읽은 항목 수: %d, 이름: %s, 나이: %d\n", n, name, age)
// Scanf — 포맷 지정 입력
var x, y float64
fmt.Print("좌표 입력 (x,y): ")
fmt.Scanf("%f,%f", &x, &y)
fmt.Printf("X: %.2f, Y: %.2f\n", x, y)
// Scanln — 한 줄 읽기
var line string
fmt.Print("한 줄 입력: ")
fmt.Scanln(&line)
fmt.Println("입력된 줄:", line)
}
fmt.Errorf로 에러 래핑
package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("항목을 찾을 수 없음")
func findUser(id int) error {
if id <= 0 {
// %w 동사로 에러 래핑 (Go 1.13+)
return fmt.Errorf("findUser: 잘못된 ID %d: %w", id, ErrNotFound)
}
return nil
}
func main() {
err := findUser(-1)
if err != nil {
fmt.Println(err) // findUser: 잘못된 ID -1: 항목을 찾을 수 없음
// errors.Is로 래핑된 에러 확인
if errors.Is(err, ErrNotFound) {
fmt.Println("NotFound 에러입니다")
}
}
}
log 패키지 — 기본 로깅
log 패키지는 타임스탬프와 함께 메시지를 출력합니다. 프로그램 종료나 패닉 동작이 내장되어 간단한 에러 처리에 유용합니다.
package main
import (
"log"
"os"
)
func main() {
// 기본 로그 함수 — 타임스탬프 자동 포함
log.Print("기본 로그 메시지")
log.Printf("사용자 %s가 로그인했습니다 (ID: %d)", "Alice", 42)
log.Println("줄바꿈 포함 로그")
// Fatal 계열 — 로그 출력 후 os.Exit(1) 호출
// log.Fatal("치명적 오류 — 프로그램 종료")
// log.Fatalf("DB 연결 실패: %v", err)
// Panic 계열 — 로그 출력 후 panic() 호출
// log.Panic("패닉 발생")
// 플래그 설정 — 로그 출력 형식 지정
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
// 출력 예: 2024/01/15 10:30:00 main.go:25: 메시지
log.Println("플래그 변경 후 로그")
// 플래그 상수들
// log.Ldate — 날짜: 2009/01/23
// log.Ltime — 시간: 01:23:23
// log.Lmicroseconds — 마이크로초
// log.Llongfile — 전체 파일 경로
// log.Lshortfile — 파일명만
// log.LUTC — UTC 시간 사용
// log.Lmsgprefix — 접두어를 메시지 앞에 배치
// 접두어 설정
log.SetPrefix("[APP] ")
log.Println("접두어 포함 로그") // [APP] 2024/01/15 ...
// 출력 대상 변경
log.SetOutput(os.Stderr) // 기본값도 stderr
}
log.New — 커스텀 로거 생성
package main
import (
"log"
"os"
)
func main() {
// 파일에 로그 쓰기
logFile, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
log.Fatal("로그 파일 열기 실패:", err)
}
defer logFile.Close()
// 커스텀 로거 — 출력 대상, 접두어, 플래그 지정
infoLogger := log.New(os.Stdout, "[INFO] ", log.Ldate|log.Ltime)
errorLogger := log.New(logFile, "[ERROR] ", log.Ldate|log.Ltime|log.Lshortfile)
warnLogger := log.New(os.Stderr, "[WARN] ", log.Ldate|log.Ltime)
infoLogger.Println("서버가 시작되었습니다")
warnLogger.Printf("메모리 사용량: %d%%", 85)
errorLogger.Println("데이터베이스 연결 오류")
}
slog 패키지 — 구조화 로깅 (Go 1.21+)
slog는 Go 1.21에 표준 라이브러리에 추가된 구조화 로깅 패키지입니다. JSON이나 텍스트 형식으로 키-값 쌍을 함께 출력해 로그 분석 도구와 연동하기 쉽습니다.
기본 사용법
package main
import (
"log/slog"
"os"
)
func main() {
// 기본 slog — 텍스트 형식으로 stderr에 출력
slog.Info("서버 시작", "port", 8080, "env", "production")
slog.Debug("디버그 메시지", "detail", "상세 정보") // 기본 레벨은 Info, 출력 안 됨
slog.Warn("경고 메시지", "threshold", 90)
slog.Error("에러 발생", "code", 500, "msg", "Internal Server Error")
// JSON 핸들러 — 로그를 JSON으로 출력
jsonHandler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug, // Debug 이상 모든 레벨 출력
})
jsonLogger := slog.New(jsonHandler)
jsonLogger.Info("JSON 로그", "user", "Alice", "action", "login")
// {"time":"2024-01-15T10:30:00Z","level":"INFO","msg":"JSON 로그","user":"Alice","action":"login"}
// 텍스트 핸들러 — 사람이 읽기 쉬운 형식
textHandler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
// AddSource: true, // 소스 파일 위치 포함
})
textLogger := slog.New(textHandler)
textLogger.Info("텍스트 로그", "user", "Bob", "score", 95.5)
// time=2024-01-15T10:30:00.000Z level=INFO msg=텍스트 로그 user=Bob score=95.5
// 기본 로거 교체
slog.SetDefault(jsonLogger)
slog.Info("이제 JSON으로 출력됨")
}
slog.With — 공통 속성 추가
package main
import (
"log/slog"
"os"
)
func main() {
baseLogger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
// With로 공통 속성을 가진 자식 로거 생성
requestLogger := baseLogger.With(
"request_id", "req-12345",
"user_id", 42,
"path", "/api/users",
)
// 모든 로그에 request_id, user_id, path가 자동 포함
requestLogger.Info("요청 처리 시작")
requestLogger.Info("DB 쿼리 실행", "query", "SELECT * FROM users")
requestLogger.Info("요청 처리 완료", "duration_ms", 123)
// slog.Attr — 타입 안전한 속성
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
logger.Info("타입 안전 속성",
slog.String("name", "Alice"),
slog.Int("age", 30),
slog.Bool("active", true),
slog.Float64("score", 98.5),
slog.Group("address",
slog.String("city", "Seoul"),
slog.String("country", "Korea"),
),
)
}
커스텀 핸들러 구현
package main
import (
"context"
"fmt"
"log/slog"
"os"
)
// ColorHandler — 레벨별 색상 출력 커스텀 핸들러
type ColorHandler struct {
opts slog.HandlerOptions
inner slog.Handler
}
func NewColorHandler(w *os.File, opts *slog.HandlerOptions) *ColorHandler {
return &ColorHandler{
inner: slog.NewTextHandler(w, opts),
}
}
// Enabled — 이 핸들러가 해당 레벨을 처리할지 여부
func (h *ColorHandler) Enabled(ctx context.Context, level slog.Level) bool {
return h.inner.Enabled(ctx, level)
}
// Handle — 실제 로그 레코드 처리
func (h *ColorHandler) Handle(ctx context.Context, r slog.Record) error {
// ANSI 색상 코드
colors := map[slog.Level]string{
slog.LevelDebug: "\033[36m", // 청록
slog.LevelInfo: "\033[32m", // 녹색
slog.LevelWarn: "\033[33m", // 노랑
slog.LevelError: "\033[31m", // 빨강
}
reset := "\033[0m"
color := colors[r.Level]
fmt.Printf("%s[%s]%s %s\n", color, r.Level, reset, r.Message)
return nil
}
// WithAttrs — 속성 추가 핸들러 반환
func (h *ColorHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
return &ColorHandler{inner: h.inner.WithAttrs(attrs)}
}
// WithGroup — 그룹 추가 핸들러 반환
func (h *ColorHandler) WithGroup(name string) slog.Handler {
return &ColorHandler{inner: h.inner.WithGroup(name)}
}
func main() {
handler := NewColorHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
})
logger := slog.New(handler)
logger.Debug("디버그 메시지")
logger.Info("정보 메시지")
logger.Warn("경고 메시지")
logger.Error("에러 메시지")
}
실전 예제 — 멀티 핸들러 로거와 요청 ID 구조화 로깅
package main
import (
"context"
"io"
"log/slog"
"os"
)
// MultiHandler — 여러 핸들러에 동시에 로그를 기록
type MultiHandler struct {
handlers []slog.Handler
}
func (m *MultiHandler) Enabled(ctx context.Context, level slog.Level) bool {
for _, h := range m.handlers {
if h.Enabled(ctx, level) {
return true
}
}
return false
}
func (m *MultiHandler) Handle(ctx context.Context, r slog.Record) error {
for _, h := range m.handlers {
if h.Enabled(ctx, r.Level) {
if err := h.Handle(ctx, r.Clone()); err != nil {
return err
}
}
}
return nil
}
func (m *MultiHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
handlers := make([]slog.Handler, len(m.handlers))
for i, h := range m.handlers {
handlers[i] = h.WithAttrs(attrs)
}
return &MultiHandler{handlers: handlers}
}
func (m *MultiHandler) WithGroup(name string) slog.Handler {
handlers := make([]slog.Handler, len(m.handlers))
for i, h := range m.handlers {
handlers[i] = h.WithGroup(name)
}
return &MultiHandler{handlers: handlers}
}
// 컨텍스트 키 타입
type contextKey string
const requestIDKey contextKey = "request_id"
// WithRequestID — 컨텍스트에 요청 ID 저장
func WithRequestID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, requestIDKey, id)
}
// LoggerFromContext — 컨텍스트에서 요청 ID를 추출해 로거에 포함
func LoggerFromContext(ctx context.Context, base *slog.Logger) *slog.Logger {
if id, ok := ctx.Value(requestIDKey).(string); ok {
return base.With("request_id", id)
}
return base
}
func main() {
// 로그 파일 생성
logFile, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
panic(err)
}
defer logFile.Close()
// 멀티 핸들러 구성 — stdout(텍스트)과 파일(JSON) 동시 출력
multiHandler := &MultiHandler{
handlers: []slog.Handler{
// 개발자용: 터미널에 텍스트 형식
slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
}),
// 운영용: 파일에 JSON 형식
slog.NewJSONHandler(io.MultiWriter(logFile, os.Stdout), &slog.HandlerOptions{
Level: slog.LevelWarn, // 경고 이상만 파일에 기록
}),
},
}
logger := slog.New(multiHandler)
slog.SetDefault(logger)
// 요청 처리 시뮬레이션
ctx := WithRequestID(context.Background(), "req-abc-123")
reqLogger := LoggerFromContext(ctx, logger)
reqLogger.Info("HTTP 요청 수신", "method", "POST", "path", "/api/orders")
reqLogger.Debug("요청 본문 파싱", "size_bytes", 1024)
reqLogger.Info("DB 트랜잭션 시작")
reqLogger.Warn("쿼리 실행 지연", "duration_ms", 2500, "threshold_ms", 1000)
reqLogger.Info("HTTP 응답 전송", "status", 201, "duration_ms", 2650)
}
패키지 선택 가이드
| 상황 | 추천 패키지 |
|---|---|
| 간단한 디버깅, 스크립트 | fmt.Println |
| 소규모 앱, 빠른 프로토타입 | log |
| 프로덕션 서버, 마이크로서비스 | slog (JSON 핸들러) |
| 로그 분석 도구 연동 | slog (JSON 핸들러) |
| 테스트 출력 | fmt + testing.T.Log |
slog는 Go 1.21 이상에서만 사용 가능합니다. 그 이전 버전을 지원해야 한다면 zerolog나 zap 같은 서드파티 라이브러리를 사용하거나 log 패키지를 사용하세요.