PHpullh

GO · 심층 가이드

Go 파일/IO 완전 정리

파일 읽기·쓰기와 bufio 스트리밍에서 시작해 JSON·YAML·CSV 처리, HTTP 클라이언트와 서버, 템플릿, database/sql까지 Go의 입출력 15가지를 다룹니다.

주제 15개 · 예제 코드 포함 · 최종 수정 2026-08-30 · 작성 pullh 편집팀

Go의 입출력은 결국 io.Readerio.Writer 두 인터페이스로 수렴합니다. 파일이든 네트워크 연결이든 메모리 버퍼든 같은 모양이라, 파일에서 읽어 HTTP 응답으로 흘려보내는 코드에 중간 변환이 거의 필요 없습니다. 이 추상화를 이해하지 못한 채 매번 전체를 바이트 슬라이스로 읽어 들이면, 작은 파일에서는 잘 돌다가 큰 입력에서 메모리를 통째로 잡아먹는 코드가 됩니다. 스트림으로 생각하는 습관이 이 영역의 핵심입니다.

파일 읽기/쓰기로 기본기를 잡고 bufio 스트리밍으로 버퍼 계층을 이해한 뒤 형식별 편으로 넘어가는 순서를 권합니다. JSON 인코딩/디코딩, YAML & TOML 처리, CSV 처리는 같은 Reader/Writer 위에 올라간 세 가지 변주입니다. 네트워크 쪽은 HTTP 클라이언트에서 시작해 HTTP 서버 심화미들웨어 체이닝으로, 출력 쪽은 템플릿 (html/template)으로 이어집니다. go:embed 지시문embed 심화는 템플릿과 정적 파일을 바이너리에 넣어 배포를 단순하게 만드는 마무리 조각입니다.

JSON에서 가장 자주 막히는 지점은 필드가 조용히 비는 현상입니다. 구조체 필드가 소문자로 시작하면 패키지 외부에서 보이지 않으므로 인코딩 대상에서 빠지고 에러도 나지 않습니다. 필드는 대문자로 시작하고 태그로 실제 키를 지정하십시오. HTTP 클라이언트 쪽에서는 응답 바디를 반드시 닫아야 합니다. 닫지 않으면 연결이 풀로 돌아가지 않아 파일 디스크립터가 새어 나갑니다. 그리고 사용자 입력을 화면에 뿌릴 때는 text/template이 아니라 html/template을 쓰십시오. 문맥별 이스케이프는 후자만 해 줍니다.

01파일 I/O & os 패키지

Go의 파일 처리. os, bufio, io 패키지를 활용합니다.

Go code

package main

import (
	"bufio"
	"fmt"
	"os"
	"strings"
)

func main() {
	// 파일 쓰기
	err := os.WriteFile("test.txt",
		[]byte("Hello\nGo\nWorld\n"), 0644)
	if err != nil { panic(err) }

	// 파일 읽기 (전체)
	data, err := os.ReadFile("test.txt")
	if err != nil { panic(err) }
	fmt.Print(string(data))

	// bufio로 줄 단위 읽기 (대용량 파일에 적합)
	f, err := os.Open("test.txt")
	if err != nil { panic(err) }
	defer f.Close()

	scanner := bufio.NewScanner(f)
	lineNum := 0
	for scanner.Scan() {
		lineNum++
		line := scanner.Text()
		fmt.Printf("%d: %s\n", lineNum, line)
	}
	if err := scanner.Err(); err != nil {
		fmt.Println("읽기 에러:", err)
	}

	// 파일 추가 쓰기 (append)
	af, err := os.OpenFile("test.txt",
		os.O_APPEND|os.O_WRONLY, 0644)
	if err != nil { panic(err) }
	defer af.Close()
	fmt.Fprintln(af, "추가된 줄")

	// strings.NewReader — 문자열을 io.Reader로
	r := strings.NewReader("hello world")
	bufReader := bufio.NewReader(r)
	word, _ := bufReader.ReadString(' ')
	fmt.Printf("첫 단어: %q
", strings.TrimSpace(word))

	// 환경 변수
	home := os.Getenv("HOME")
	fmt.Println("홈 디렉토리:", home)

	// 임시 파일
	tmp, err := os.CreateTemp("", "myapp-*.txt")
	if err != nil { panic(err) }
	defer os.Remove(tmp.Name())
	fmt.Println("임시 파일:", tmp.Name())
	tmp.Close()
}
알아두면 좋은 점

os.ReadFile()은 파일 전체를 메모리에 올립니다. 대용량 파일은 bufio.Scanner로 스트리밍 처리하세요.

자주 하는 실수

os.Open()으로 열린 파일은 읽기 전용입니다. 쓰기가 필요하면 os.OpenFile()os.O_WRONLY/os.O_RDWR 플래그를 사용하세요.

02encoding/json — 직렬화

Go의 JSON 인코딩/디코딩. 구조체 태그로 필드명을 제어합니다.

Go code

package main

import (
	"encoding/json"
	"fmt"
)

type Address struct {
	City    string `json:"city"`
	Country string `json:"country"`
}

type User struct {
	ID        int       `json:"id"`
	Name      string    `json:"name"`
	Email     string    `json:"email,omitempty"`  // 빈 값 생략
	Password  string    `json:"-"`                // JSON 제외
	Address   Address   `json:"address"`
	Tags      []string  `json:"tags,omitempty"`
}

func main() {
	// 인코딩 (Marshal)
	user := User{
		ID:    1,
		Name:  "Alice",
		Email: "alice@test.com",
		Address: Address{"Seoul", "Korea"},
		Tags:  []string{"admin", "dev"},
	}

	data, err := json.Marshal(user)
	if err != nil { panic(err) }
	fmt.Println(string(data))

	// 들여쓰기 있는 JSON
	pretty, _ := json.MarshalIndent(user, "", "  ")
	fmt.Println(string(pretty))

	// 디코딩 (Unmarshal)
	jsonStr := `{"id":2,"name":"Bob","address":{"city":"Busan","country":"Korea"}}`
	var decoded User
	if err := json.Unmarshal([]byte(jsonStr), &decoded); err != nil {
		panic(err)
	}
	fmt.Printf("이름: %s, 도시: %s
", decoded.Name, decoded.Address.City)

	// 동적 JSON — map 사용
	var dynamic map[string]interface{}
	json.Unmarshal([]byte(jsonStr), &dynamic)
	fmt.Println("name:", dynamic["name"])

	// json.Decoder — 스트림 디코딩
	// decoder := json.NewDecoder(r)
	// decoder.Decode(&obj)
}
알아두면 좋은 점

json:"-" 태그는 필드를 JSON에서 완전히 제외합니다. 비밀번호 같은 민감 정보에 사용하세요.

자주 하는 실수

json.Unmarshal에 포인터를 전달하지 않으면(&obj 대신 obj) 데이터가 채워지지 않습니다.

03go:embed 지시문

Go 1.16+의 //go:embed 지시문은 파일이나 디렉터리를 바이너리에 직접 포함합니다. 설정 파일, HTML 템플릿, 정적 자산을 별도 배포 없이 단일 바이너리로 패키징할 수 있습니다.

Go code

package main

import (
	"embed"
	"fmt"
	"io/fs"
	"net/http"
	"text/template"
)

// 1. 단일 파일 임베딩 (문자열)
//go:embed config.txt
var configStr string

// 2. 단일 파일 임베딩 (바이트 슬라이스)
//go:embed logo.png
var logoBytes []byte

// 3. 디렉터리 임베딩
//go:embed templates/*
var templateFS embed.FS

// 4. 여러 패턴 임베딩
//go:embed static/*.css static/*.js
var staticFS embed.FS

// 실전 예: 템플릿 렌더링
func renderTemplate(name string, data interface{}) (string, error) {
	tmplData, err := templateFS.ReadFile("templates/" + name)
	if err != nil {
		return "", fmt.Errorf("템플릿 읽기 실패: %w", err)
	}
	tmpl, err := template.New(name).Parse(string(tmplData))
	if err != nil {
		return "", fmt.Errorf("템플릿 파싱 실패: %w", err)
	}
	var buf strings.Builder
	err = tmpl.Execute(&buf, data)
	return buf.String(), err
}

// 실전 예: 정적 파일 서버
func staticFileServer() http.Handler {
	// embed.FS에서 서브디렉터리 추출
	subFS, _ := fs.Sub(staticFS, "static")
	return http.FileServer(http.FS(subFS))
}

func main() {
	// 임베딩된 파일 정보
	fmt.Println("=== 설정 파일 ===")
	fmt.Println(configStr)

	fmt.Printf("\n=== 로고 크기: %d bytes ===\n", len(logoBytes))

	// 임베딩된 디렉터리 순회
	fmt.Println("\n=== 임베딩된 파일 목록 ===")
	fs.WalkDir(templateFS, ".", func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if !d.IsDir() {
			info, _ := d.Info()
			fmt.Printf("  %s (%d bytes)\n", path, info.Size())
		}
		return nil
	})

	// HTTP 서버 예제
	fmt.Println("\n정적 파일 서버: http://localhost:8080/static/")
	// http.Handle("/static/", http.StripPrefix("/static/", staticFileServer()))
	// http.ListenAndServe(":8080", nil)
}
알아두면 좋은 점

embed.FSio/fs.FS 인터페이스를 구현하므로 http.FS(), template.ParseFS() 등 표준 라이브러리와 바로 연동됩니다.

자주 하는 실수

//go:embed 주석과 변수 선언 사이에 빈 줄이 있으면 작동하지 않습니다. 또한 ..를 포함하는 경로나 숨김 파일(dot file)은 기본적으로 임베딩되지 않습니다.

04파일 읽기/쓰기

osio 패키지로 파일을 읽고 씁니다. Go 1.16+의 os.ReadFile/os.WriteFile이 가장 간단합니다.

Go code

package main

import (
	"fmt"
	"os"
)

func main() {
	// 파일 쓰기 (간단한 방법)
	data := []byte("Hello, Go 파일!")
	err := os.WriteFile("test.txt", data, 0644)
	if err != nil { panic(err) }

	// 파일 읽기 (간단한 방법)
	content, err := os.ReadFile("test.txt")
	if err != nil { panic(err) }
	fmt.Println(string(content))

	// os.OpenFile로 세밀한 제어
	f, err := os.OpenFile("log.txt",
		os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil { panic(err) }
	defer f.Close()

	f.WriteString("로그 메시지\n")
	fmt.Println("파일 작성 완료")

	// 정리
	os.Remove("test.txt")
	os.Remove("log.txt")
}
알아두면 좋은 점

os.ReadFile/os.WriteFile은 작은 파일에 적합합니다. 대용량 파일은 bufio로 스트리밍 처리하세요.

자주 하는 실수

defer f.Close()의 에러를 무시하면 쓰기 버퍼가 플러시되지 않을 수 있습니다. 쓰기 파일은 Close 에러를 확인하세요.

05bufio 스트리밍

bufio로 대용량 파일을 라인 단위로 효율적으로 읽습니다. 메모리를 절약하는 스트리밍 처리입니다.

Go code

package main

import (
	"bufio"
	"fmt"
	"os"
	"strings"
)

func main() {
	// 문자열 리더로 데모
	input := "첫 번째 줄\n두 번째 줄\n세 번째 줄\n"
	reader := bufio.NewReader(strings.NewReader(input))

	for {
		line, err := reader.ReadString('\n')
		line = strings.TrimRight(line, "\n")
		if line != "" {
			fmt.Println("읽음:", line)
		}
		if err != nil { break }
	}

	// Scanner (줄 단위 읽기에 최적)
	scanner := bufio.NewScanner(strings.NewReader(input))
	scanner.Buffer(make([]byte, 1024*1024), 1024*1024) // 최대 1MB
	lineNum := 0
	for scanner.Scan() {
		lineNum++
		fmt.Printf("%d: %s\n", lineNum, scanner.Text())
	}
	if err := scanner.Err(); err != nil {
		fmt.Fprintln(os.Stderr, "읽기 에러:", err)
	}
}
알아두면 좋은 점

bufio.Scanner의 기본 버퍼는 64KB입니다. 긴 줄이 있으면 scanner.Buffer()로 크기를 늘리세요.

자주 하는 실수

Scanner.Err()를 확인하지 않으면 버퍼 초과 등의 에러를 놓칩니다. 루프 후 반드시 에러를 검사하세요.

06JSON 인코딩/디코딩

encoding/json으로 구조체와 JSON을 변환합니다. 태그로 필드 이름과 동작을 제어합니다.

Go code

package main

import (
	"encoding/json"
	"fmt"
)

type User struct {
	ID       int      `json:"id"`
	Name     string   `json:"name"`
	Email    string   `json:"email,omitempty"`
	Password string   `json:"-"`          // JSON에서 제외
	Tags     []string `json:"tags,omitempty"`
}

func main() {
	// 마샬링 (구조체 → JSON)
	user := User{ID: 1, Name: "Gopher", Password: "secret"}
	data, _ := json.MarshalIndent(user, "", "  ")
	fmt.Println(string(data))

	// 언마샬링 (JSON → 구조체)
	jsonStr := `{"id":2,"name":"Rustacean","email":"r@go.dev","tags":["dev"]}`
	var u2 User
	json.Unmarshal([]byte(jsonStr), &u2)
	fmt.Printf("%+v\n", u2)

	// map으로 동적 JSON 처리
	var m map[string]any
	json.Unmarshal([]byte(jsonStr), &m)
	fmt.Println("동적:", m["name"])
}
알아두면 좋은 점

omitempty는 제로 값(0, "", nil, 빈 슬라이스)을 JSON에서 생략합니다. API 응답 크기를 줄이는 데 유용합니다.

자주 하는 실수

비공개 필드(소문자)는 JSON 인코딩/디코딩에서 무시됩니다. JSON 바인딩이 필요한 필드는 대문자로 시작해야 합니다.

07YAML & TOML 처리

설정 파일에 많이 사용되는 YAML과 TOML 형식을 Go에서 처리합니다.

Go code

package main

import "fmt"

// YAML 예시 (gopkg.in/yaml.v3 사용)
// import "gopkg.in/yaml.v3"
type Config struct {
	Server struct {
		Host string `yaml:"host"`
		Port int    `yaml:"port"`
	} `yaml:"server"`
	Database struct {
		DSN     string `yaml:"dsn"`
		MaxConn int    `yaml:"max_conn"`
	} `yaml:"database"`
}

func main() {
	// YAML 문자열 예시
	yamlData := `
server:
  host: localhost
  port: 8080
database:
  dsn: postgres://localhost/mydb
  max_conn: 10
`

	// TOML 문자열 예시
	tomlData := `
[server]
host = "localhost"
port = 8080

[database]
dsn = "postgres://localhost/mydb"
max_conn = 10
`

	fmt.Println("YAML 설정:", yamlData)
	fmt.Println("TOML 설정:", tomlData)
	// 실제 파싱: yaml.Unmarshal([]byte(yamlData), &config)
	// 실제 파싱: toml.Unmarshal([]byte(tomlData), &config)
}
알아두면 좋은 점

YAML은 gopkg.in/yaml.v3, TOML은 github.com/BurntSushi/toml이 표준 라이브러리입니다.

자주 하는 실수

YAML의 들여쓰기에 탭을 사용하면 파싱 에러가 발생합니다. YAML은 반드시 스페이스로 들여쓰기해야 합니다.

08CSV 처리

encoding/csv로 CSV 파일을 읽고 씁니다. 구조체 매핑과 스트리밍 처리를 다룹니다.

Go code

package main

import (
	"encoding/csv"
	"fmt"
	"strings"
)

func main() {
	// CSV 읽기
	csvData := "이름,나이,도시\n김철수,30,서울\n이영희,25,부산\n"
	reader := csv.NewReader(strings.NewReader(csvData))

	// 헤더 읽기
	header, _ := reader.Read()
	fmt.Println("헤더:", header)

	// 모든 레코드 읽기
	records, _ := reader.ReadAll()
	for _, record := range records {
		fmt.Printf("이름: %s, 나이: %s, 도시: %s\n",
			record[0], record[1], record[2])
	}

	// CSV 쓰기
	var buf strings.Builder
	writer := csv.NewWriter(&buf)
	writer.Write([]string{"제품", "가격", "수량"})
	writer.Write([]string{"Go 책", "35000", "10"})
	writer.Write([]string{"키보드", "89000", "5"})
	writer.Flush()

	fmt.Println("\n생성된 CSV:")
	fmt.Println(buf.String())
}
알아두면 좋은 점

대용량 CSV는 ReadAll 대신 Read()를 루프로 호출하여 한 줄씩 스트리밍 처리하세요.

자주 하는 실수

csv.Writer 사용 후 Flush()를 호출하지 않으면 데이터가 버퍼에 남아 출력되지 않습니다.

09HTTP 클라이언트

net/http로 HTTP 요청을 보냅니다. 타임아웃 설정과 응답 처리를 다룹니다.

Go code

package main

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"time"
)

func main() {
	// 커스텀 클라이언트 (타임아웃 필수!)
	client := &http.Client{
		Timeout: 10 * time.Second,
	}

	// GET 요청
	resp, err := client.Get("https://httpbin.org/get")
	if err != nil {
		fmt.Println("에러:", err)
		return
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	fmt.Printf("상태: %d, 크기: %d bytes\n", resp.StatusCode, len(body))

	// context로 취소 가능한 요청
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	req, _ := http.NewRequestWithContext(ctx, "GET", "https://httpbin.org/get", nil)
	req.Header.Set("Accept", "application/json")

	resp2, err := client.Do(req)
	if err != nil {
		fmt.Println("에러:", err)
		return
	}
	defer resp2.Body.Close()
	fmt.Println("상태:", resp2.StatusCode)
}
알아두면 좋은 점

http.DefaultClient는 타임아웃이 없습니다! 항상 타임아웃을 설정한 커스텀 클라이언트를 사용하세요.

자주 하는 실수

resp.Body를 닫지 않으면 TCP 커넥션이 재사용되지 않아 커넥션 누수가 발생합니다. 항상 defer resp.Body.Close()를 사용하세요.

10HTTP 서버 심화

net/http로 구조화된 HTTP 서버를 구현합니다. 라우팅, 미들웨어, 그레이스풀 셧다운을 다룹니다.

Go code

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"os/signal"
	"syscall"
	"time"
)

func jsonResponse(w http.ResponseWriter, status int, data any) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	json.NewEncoder(w).Encode(data)
}

func main() {
	mux := http.NewServeMux()

	// Go 1.22+ 패턴 매칭
	mux.HandleFunc("GET /api/health", func(w http.ResponseWriter, r *http.Request) {
		jsonResponse(w, 200, map[string]string{"status": "ok"})
	})

	mux.HandleFunc("GET /api/users/{id}", func(w http.ResponseWriter, r *http.Request) {
		id := r.PathValue("id")
		jsonResponse(w, 200, map[string]string{"id": id})
	})

	srv := &http.Server{
		Addr:         ":8080",
		Handler:      mux,
		ReadTimeout:  5 * time.Second,
		WriteTimeout: 10 * time.Second,
	}

	// 그레이스풀 셧다운
	ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
	defer stop()

	go func() { fmt.Println("서버 시작 :8080"); srv.ListenAndServe() }()
	<-ctx.Done()
	shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	srv.Shutdown(shutdownCtx)
	fmt.Println("서버 종료")
}
알아두면 좋은 점

Go 1.22부터 http.ServeMux"GET /api/users/{id}" 형식의 패턴 매칭을 지원합니다.

자주 하는 실수

ReadTimeoutWriteTimeout을 설정하지 않으면 Slowloris 공격에 취약합니다. 반드시 타임아웃을 설정하세요.

11미들웨어 체이닝 (HTTP)

HTTP 미들웨어를 체이닝하여 로깅, CORS, 인증 등 횡단 관심사를 분리합니다.

Go code

package main

import (
	"fmt"
	"log/slog"
	"net/http"
	"time"
)

type Middleware func(http.Handler) http.Handler

func Chain(h http.Handler, mws ...Middleware) http.Handler {
	for i := len(mws) - 1; i >= 0; i-- {
		h = mws[i](h)
	}
	return h
}

func Logger(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()
		next.ServeHTTP(w, r)
		slog.Info("요청", "method", r.Method,
			"path", r.URL.Path, "duration", time.Since(start))
	})
}

func CORS(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Access-Control-Allow-Origin", "*")
		w.Header().Set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE")
		if r.Method == "OPTIONS" {
			w.WriteHeader(204)
			return
		}
		next.ServeHTTP(w, r)
	})
}

func Recovery(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer func() {
			if err := recover(); err != nil {
				slog.Error("패닉 복구", "error", err)
				http.Error(w, "Internal Server Error", 500)
			}
		}()
		next.ServeHTTP(w, r)
	})
}

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "Hello!")
	})

	handler := Chain(mux, Recovery, Logger, CORS)
	fmt.Println("미들웨어 체인: Recovery → Logger → CORS → Handler")
	_ = handler // http.ListenAndServe(":8080", handler)
}
알아두면 좋은 점

미들웨어 순서가 중요합니다. Recovery는 가장 바깥에, Logger는 그 다음에 배치하여 모든 패닉과 요청을 캡처하세요.

자주 하는 실수

미들웨어에서 next.ServeHTTP를 호출하지 않으면 체인이 중단됩니다. 의도적 중단(인증 실패)이 아니면 반드시 호출하세요.

12템플릿 (html/template)

html/template으로 안전한 HTML을 생성합니다. XSS 자동 방지와 템플릿 상속을 지원합니다.

Go code

package main

import (
	"html/template"
	"os"
)

const tmpl = `<!DOCTYPE html>
<html>
<head><title>{{.Title}}</title></head>
<body>
  <h1>{{.Title}}</h1>
  {{range .Items}}
    <div class="item">
      <strong>{{.Name}}</strong>: {{.Price}}원
    </div>
  {{end}}
  {{if .ShowFooter}}
    <footer>© 2024</footer>
  {{end}}
  <!-- XSS 자동 이스케이프 -->
  <p>입력: {{.UserInput}}</p>
</body>
</html>`

type PageData struct {
	Title      string
	Items      []struct{ Name string; Price int }
	ShowFooter bool
	UserInput  string
}

func main() {
	t := template.Must(template.New("page").Parse(tmpl))

	data := PageData{
		Title: "상품 목록",
		Items: []struct{ Name string; Price int }{
			{"Go 책", 35000}, {"키보드", 89000},
		},
		ShowFooter: true,
		UserInput:  "<script>alert('xss')</script>",
	}

	t.Execute(os.Stdout, data)
}
알아두면 좋은 점

html/template은 컨텍스트에 따라 자동으로 HTML, JS, URL 이스케이프를 적용합니다. XSS를 원천 차단합니다.

자주 하는 실수

text/template은 이스케이프를 하지 않습니다. HTML 출력에는 반드시 html/template을 사용하세요.

13database/sql 사용법

database/sql로 데이터베이스에 연결하고 CRUD를 수행합니다. 커넥션 풀링과 트랜잭션을 다룹니다.

Go code

package main

import (
	"context"
	"database/sql"
	"fmt"
	"time"
	// _ "github.com/lib/pq" // PostgreSQL 드라이버
)

func example(db *sql.DB) {
	ctx := context.Background()

	// 커넥션 풀 설정
	db.SetMaxOpenConns(25)
	db.SetMaxIdleConns(5)
	db.SetConnMaxLifetime(5 * time.Minute)

	// 단일 행 조회
	var name string
	err := db.QueryRowContext(ctx,
		"SELECT name FROM users WHERE id = $1", 1).Scan(&name)
	if err == sql.ErrNoRows {
		fmt.Println("사용자 없음")
	} else if err != nil {
		fmt.Println("에러:", err)
	}

	// 여러 행 조회
	rows, _ := db.QueryContext(ctx, "SELECT id, name FROM users")
	defer rows.Close()
	for rows.Next() {
		var id int
		var n string
		rows.Scan(&id, &n)
		fmt.Printf("ID: %d, Name: %s\n", id, n)
	}

	// 트랜잭션
	tx, _ := db.BeginTx(ctx, nil)
	defer tx.Rollback() // 커밋 안 되면 자동 롤백
	tx.ExecContext(ctx, "INSERT INTO users(name) VALUES($1)", "Gopher")
	tx.Commit()
}

func main() {
	// db, _ := sql.Open("postgres", "postgres://localhost/mydb?sslmode=disable")
	// defer db.Close()
	// example(db)
	fmt.Println("database/sql 패턴: 커넥션 풀, 쿼리, 트랜잭션")
}
알아두면 좋은 점

항상 Context 버전 메서드(QueryContext, ExecContext)를 사용하여 타임아웃과 취소를 지원하세요.

자주 하는 실수

rows.Close()를 호출하지 않으면 커넥션이 풀에 반환되지 않아 커넥션 고갈이 발생합니다.

14embed 심화

//go:embed로 파일을 바이너리에 포함합니다. 정적 자산, 마이그레이션 SQL, 템플릿 등에 활용합니다.

Go code

package main

import (
	"embed"
	"fmt"
	"io/fs"
)

// 단일 파일 임베딩
//go:embed go.mod
var modFile string

// 여러 파일 임베딩
//go:embed data/*.sql
var sqlFiles embed.FS

// 전체 디렉토리
//go:embed static
var staticFS embed.FS

func main() {
	// 문자열로 직접 사용
	fmt.Println("go.mod 내용:")
	fmt.Println(modFile)

	// embed.FS로 파일 시스템 접근
	entries, _ := fs.ReadDir(sqlFiles, "data")
	for _, e := range entries {
		fmt.Println("SQL 파일:", e.Name())
		content, _ := sqlFiles.ReadFile("data/" + e.Name())
		fmt.Println(string(content))
	}

	// http.FS와 연동
	// http.Handle("/static/",
	//     http.StripPrefix("/static/",
	//         http.FileServer(http.FS(staticFS))))
	fmt.Println("embed.FS는 io/fs.FS 인터페이스를 구현합니다")
}
알아두면 좋은 점

embed.FSio/fs.FS를 구현하므로 http.FS(), template.ParseFS() 등과 바로 연동됩니다.

자주 하는 실수

//go:embed 디렉티브와 변수 선언 사이에 빈 줄이 있으면 동작하지 않습니다. 바로 다음 줄에 var를 선언하세요.

15HTTP 서버 고급 패턴

구조화된 핸들러, 의존성 주입, 에러 처리를 결합한 프로덕션급 HTTP 서버 패턴입니다.

Go code

package main

import (
	"encoding/json"
	"fmt"
	"net/http"
)

// 에러를 반환하는 핸들러
type AppHandler func(w http.ResponseWriter, r *http.Request) error

func (fn AppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if err := fn(w, r); err != nil {
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(500)
		json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
	}
}

// 핸들러 그룹 (의존성 주입)
type Handlers struct {
	// db *sql.DB  // 실제 의존성
}

func (h *Handlers) GetUser(w http.ResponseWriter, r *http.Request) error {
	id := r.PathValue("id")
	if id == "" {
		http.Error(w, "ID 필요", 400)
		return nil
	}
	w.Header().Set("Content-Type", "application/json")
	return json.NewEncoder(w).Encode(map[string]string{
		"id": id, "name": "Gopher",
	})
}

func (h *Handlers) CreateUser(w http.ResponseWriter, r *http.Request) error {
	var input struct{ Name string }
	if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
		http.Error(w, "잘못된 JSON", 400)
		return nil
	}
	w.WriteHeader(201)
	return json.NewEncoder(w).Encode(input)
}

func main() {
	h := &Handlers{}
	mux := http.NewServeMux()
	mux.Handle("GET /api/users/{id}", AppHandler(h.GetUser))
	mux.Handle("POST /api/users", AppHandler(h.CreateUser))

	fmt.Println("구조화된 HTTP 서버 패턴")
	// http.ListenAndServe(":8080", mux)
}
알아두면 좋은 점

AppHandler 패턴은 핸들러가 에러를 반환하게 하여 중앙 집중 에러 처리를 가능하게 합니다.

자주 하는 실수

json.NewDecoder(r.Body)에서 DisallowUnknownFields()를 호출하지 않으면 잘못된 필드를 조용히 무시합니다.

정리하며

  • 큰 입력은 통째로 읽지 말고 Reader/Writer 스트림으로 흘려보냅니다
  • JSON 대상 구조체 필드는 대문자로 시작해야 하며 키 이름은 태그로 지정합니다
  • HTTP 응답 바디를 닫지 않으면 연결이 반환되지 않아 디스크립터가 샙니다
  • 사용자 입력이 섞인 출력은 html/template으로 렌더링해 이스케이프를 맡깁니다

더 깊이 들어가고 싶다면 Go 학습 라이브러리에서 다른 주제 가이드를 이어서 보거나, 언어 비교에서 같은 개념이 다른 언어에서 어떻게 표현되는지 확인해 보세요.