- Published on
Go 언어 완전 가이드 2025: 고루틴, 채널, 인터페이스부터 프로덕션 패턴까지
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 도입: 2025년, 왜 Go인가?
- 1. Go 기초: 타입, 구조체, 메서드, 포인터
- 2. 인터페이스와 컴포지션
- 3. 동시성: 고루틴과 채널
- 4. 동시성 패턴
- 5. 에러 처리
- 6. 제네릭 (Go 1.18+)
- 7. 테스트
- 8. REST API 구축
- 9. gRPC 서비스 구축
- 10. CLI 도구 개발
- 11. 프로덕션 배포
- 12. Go vs Rust vs Java 비교
- 13. 면접 질문 15선
- 14. 실전 퀴즈
- 참고 자료
도입: 2025년, 왜 Go인가?
Kubernetes, Docker, Terraform, Prometheus, Istio — 클라우드 네이티브 생태계의 핵심 도구들이 모두 Go로 작성되었습니다. 2025년 Stack Overflow 설문에서 Go는 "가장 배우고 싶은 언어" 3위, "가장 연봉이 높은 언어" 4위에 올랐으며, Indeed 기준 Go 개발자 구인은 전년 대비 15% 증가했습니다.
Go가 선택받는 이유는 명확합니다:
- 단순함: 키워드 25개, 클래스 없음, 상속 없음
- 동시성: 고루틴과 채널로 수백만 동시 작업 처리
- 빠른 컴파일: 수백만 줄 코드도 수 초 내 컴파일
- 단일 바이너리: 의존성 없는 정적 바이너리로 배포 극도로 단순
- 성능: C/C++에 근접한 실행 속도, Java/Python보다 메모리 효율적
Go 사용 기업 및 프로젝트:
┌─────────────────────────────────────────────────────────┐
│ Google - 내부 인프라, gRPC, Kubernetes 원저자 │
│ Uber - 고성능 지오펜싱 서비스 (Go로 재작성) │
│ Cloudflare - 엣지 프록시, DNS, Workers 런타임 │
│ Twitch - 실시간 채팅 시스템 (수백만 동시 접속) │
│ Docker - 컨테이너 런타임 전체가 Go │
│ Terraform - HashiCorp 전 제품군이 Go │
│ CockroachDB - 분산 SQL 데이터베이스 │
│ Dropbox - 성능 핵심 서비스를 Python에서 Go로 마이그레이션 │
└─────────────────────────────────────────────────────────┘
이 가이드에서는 Go의 기초 문법부터 동시성 패턴, 프로덕션 레벨 서버 구축, CLI 도구 개발, Docker 최적화까지 체계적으로 다룹니다.
1. Go 기초: 타입, 구조체, 메서드, 포인터
1.1 기본 타입과 제로 값
Go의 모든 변수는 선언 시 제로 값(zero value)으로 초기화됩니다:
package main
import "fmt"
func main() {
// 기본 타입과 제로 값
var i int // 0
var f float64 // 0.0
var b bool // false
var s string // "" (빈 문자열)
// 짧은 선언 (타입 추론)
name := "gopher" // string
age := 10 // int
pi := 3.14 // float64
active := true // bool
// 상수
const MaxRetries = 3
const (
StatusOK = 200
StatusError = 500
)
fmt.Println(i, f, b, s, name, age, pi, active)
}
1.2 슬라이스 vs 배열
배열은 고정 크기이고, 슬라이스는 동적 크기의 배열 뷰입니다:
// 배열: 크기가 타입의 일부
var arr [5]int // [0, 0, 0, 0, 0]
matrix := [2][3]int{{1,2,3}, {4,5,6}}
// 슬라이스: 동적, 실무에서 99% 사용
slice := []int{1, 2, 3}
slice = append(slice, 4, 5) // [1, 2, 3, 4, 5]
// make로 생성 (길이, 용량)
buf := make([]byte, 0, 1024) // 길이 0, 용량 1024
// 슬라이싱 (원본 공유 주의!)
original := []int{1, 2, 3, 4, 5}
sub := original[1:3] // [2, 3] - 원본의 뷰
sub[0] = 99 // original도 변경됨: [1, 99, 3, 4, 5]
// 안전한 복사
copied := make([]int, len(original))
copy(copied, original)
1.3 맵(Map)
// 맵 생성
m := map[string]int{
"apple": 5,
"banana": 3,
}
// 존재 여부 확인 (comma ok 패턴)
val, ok := m["cherry"]
if !ok {
fmt.Println("cherry not found")
}
// 순회 (순서 비보장)
for key, value := range m {
fmt.Printf("%s: %d\n", key, value)
}
// 삭제
delete(m, "apple")
1.4 구조체와 메서드
type User struct {
ID int
Name string
Email string
CreatedAt time.Time
}
// 값 수신자 메서드 (읽기 전용)
func (u User) FullName() string {
return u.Name
}
// 포인터 수신자 메서드 (수정 가능)
func (u *User) UpdateEmail(email string) {
u.Email = email
}
// 생성자 패턴 (Go에는 생성자가 없으므로 함수로 대체)
func NewUser(name, email string) *User {
return &User{
Name: name,
Email: email,
CreatedAt: time.Now(),
}
}
1.5 포인터
Go의 포인터는 C와 달리 포인터 연산이 불가능하여 안전합니다:
func main() {
x := 42
p := &x // 주소 획득
fmt.Println(*p) // 역참조: 42
*p = 100
fmt.Println(x) // 100
// nil 포인터 체크
var ptr *int
if ptr != nil {
fmt.Println(*ptr)
}
}
// 값 전달 vs 포인터 전달
func doubleValue(n int) { n *= 2 } // 원본 불변
func doublePointer(n *int) { *n *= 2 } // 원본 수정
2. 인터페이스와 컴포지션
2.1 암시적 인터페이스 (덕 타이핑)
Go의 인터페이스는 Java/C#과 달리 명시적 선언이 필요 없습니다. 메서드를 구현하면 자동으로 인터페이스를 만족합니다:
// 인터페이스 정의
type Writer interface {
Write(p []byte) (n int, err error)
}
type Reader interface {
Read(p []byte) (n int, err error)
}
// 인터페이스 조합
type ReadWriter interface {
Reader
Writer
}
// MyBuffer는 Writer를 "명시적으로 선언하지 않아도" 만족
type MyBuffer struct {
data []byte
}
func (b *MyBuffer) Write(p []byte) (int, error) {
b.data = append(b.data, p...)
return len(p), nil
}
// io.Writer를 받는 함수에 MyBuffer 전달 가능
func SaveToWriter(w Writer, content string) error {
_, err := w.Write([]byte(content))
return err
}
2.2 작은 인터페이스의 힘
Go 표준 라이브러리의 핵심 철학은 작은 인터페이스입니다:
Go 표준 라이브러리 핵심 인터페이스:
┌──────────────────────────────────────────────┐
│ io.Reader - Read(p []byte) (n, err) │
│ io.Writer - Write(p []byte) (n, err) │
│ io.Closer - Close() error │
│ fmt.Stringer - String() string │
│ error - Error() string │
│ sort.Interface - Len, Less, Swap │
│ http.Handler - ServeHTTP(w, r) │
│ json.Marshaler - MarshalJSON() ([]byte, err) │
└──────────────────────────────────────────────┘
// 실용 예: 어떤 Reader든 받아서 처리
func CountLines(r io.Reader) (int, error) {
scanner := bufio.NewScanner(r)
count := 0
for scanner.Scan() {
count++
}
return count, scanner.Err()
}
// 파일, HTTP 응답, 문자열 등 모두 가능
lines1, _ := CountLines(os.Stdin)
lines2, _ := CountLines(resp.Body)
lines3, _ := CountLines(strings.NewReader("hello\nworld"))
2.3 구조체 임베딩 (컴포지션)
Go는 상속 대신 임베딩으로 코드를 재사용합니다:
type Animal struct {
Name string
}
func (a Animal) Speak() string {
return a.Name + " makes a sound"
}
// Dog은 Animal을 임베딩 (상속이 아닌 컴포지션)
type Dog struct {
Animal // 임베딩
Breed string
}
func main() {
d := Dog{
Animal: Animal{Name: "Buddy"},
Breed: "Labrador",
}
fmt.Println(d.Speak()) // Animal의 메서드 승격
fmt.Println(d.Name) // Animal의 필드 직접 접근
}
3. 동시성: 고루틴과 채널
3.1 고루틴
고루틴은 Go 런타임이 관리하는 경량 스레드입니다. OS 스레드가 약 1MB 스택인 반면, 고루틴은 약 2KB에서 시작합니다:
func main() {
// 고루틴 시작: go 키워드만 붙이면 됨
go func() {
fmt.Println("Hello from goroutine!")
}()
// 1000개 고루틴 동시 실행
for i := 0; i < 1000; i++ {
go func(id int) {
fmt.Printf("Worker %d\n", id)
}(i) // i를 인자로 전달 (클로저 변수 캡처 주의)
}
time.Sleep(time.Second) // 메인 고루틴 대기
}
3.2 채널 (Channel)
채널은 고루틴 간 데이터를 안전하게 전달하는 파이프입니다:
func main() {
// 버퍼 없는 채널 (동기)
ch := make(chan string)
go func() {
ch <- "hello" // 전송 (수신자가 올 때까지 블록)
}()
msg := <-ch // 수신 (전송자가 올 때까지 블록)
fmt.Println(msg)
// 버퍼 있는 채널 (비동기)
buffered := make(chan int, 3)
buffered <- 1 // 블록 안 됨 (버퍼에 여유)
buffered <- 2
buffered <- 3
// buffered <- 4 // 여기서 블록됨 (버퍼 가득)
// 채널 방향 제한
// chan<- int (전송 전용)
// <-chan int (수신 전용)
}
// 생산자-소비자 패턴
func producer(ch chan<- int) {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch) // 더 이상 보낼 데이터 없음
}
func consumer(ch <-chan int) {
for val := range ch { // close될 때까지 수신
fmt.Println(val)
}
}
3.3 Select 문
select는 여러 채널 연산을 동시에 대기합니다:
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "from ch1"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "from ch2"
}()
// 먼저 준비된 채널 수신
select {
case msg := <-ch1:
fmt.Println(msg)
case msg := <-ch2:
fmt.Println(msg)
case <-time.After(3 * time.Second):
fmt.Println("timeout!")
}
}
3.4 sync 패키지
// WaitGroup: 여러 고루틴 완료 대기
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Printf("Worker %d done\n", id)
}(i)
}
wg.Wait() // 모든 고루틴 완료까지 대기
}
// Mutex: 공유 자원 보호
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
// Once: 한 번만 실행
var once sync.Once
var instance *Database
func GetDB() *Database {
once.Do(func() {
instance = connectDB()
})
return instance
}
3.5 context.Context
context는 고루틴의 수명을 관리하고, 취소 신호와 타임아웃을 전파합니다:
func fetchURL(ctx context.Context, url string) (string, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return "", err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
return string(body), err
}
func main() {
// 3초 타임아웃
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
result, err := fetchURL(ctx, "https://api.example.com/data")
if errors.Is(err, context.DeadlineExceeded) {
fmt.Println("Request timed out!")
return
}
fmt.Println(result)
}
4. 동시성 패턴
4.1 Fan-Out / Fan-In
여러 고루틴이 작업을 분산 처리(Fan-Out)하고 결과를 하나로 모으는(Fan-In) 패턴:
func fanOut(input <-chan int, workers int) []<-chan int {
channels := make([]<-chan int, workers)
for i := 0; i < workers; i++ {
channels[i] = process(input)
}
return channels
}
func process(input <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range input {
out <- n * n // 제곱 연산
}
}()
return out
}
func fanIn(channels ...<-chan int) <-chan int {
var wg sync.WaitGroup
merged := make(chan int)
for _, ch := range channels {
wg.Add(1)
go func(c <-chan int) {
defer wg.Done()
for val := range c {
merged <- val
}
}(ch)
}
go func() {
wg.Wait()
close(merged)
}()
return merged
}
4.2 워커 풀 (Worker Pool)
func workerPool(jobs <-chan Job, results chan<- Result, numWorkers int) {
var wg sync.WaitGroup
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func(workerID int) {
defer wg.Done()
for job := range jobs {
result := processJob(job)
results <- result
}
}(i)
}
go func() {
wg.Wait()
close(results)
}()
}
4.3 파이프라인
func generator(nums ...int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for _, n := range nums {
out <- n
}
}()
return out
}
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
out <- n * n
}
}()
return out
}
func filter(in <-chan int, predicate func(int) bool) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
if predicate(n) {
out <- n
}
}
}()
return out
}
// 사용: 파이프라인 조합
func main() {
nums := generator(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
squares := square(nums)
evens := filter(squares, func(n int) bool { return n%2 == 0 })
for result := range evens {
fmt.Println(result) // 4, 16, 36, 64, 100
}
}
4.4 Rate Limiter와 세마포어
// Rate Limiter
func rateLimitedFetch(urls []string, rps int) {
ticker := time.NewTicker(time.Second / time.Duration(rps))
defer ticker.Stop()
for _, url := range urls {
<-ticker.C // 초당 rps번만 실행
go fetch(url)
}
}
// 세마포어 패턴 (동시 실행 수 제한)
func semaphorePattern(tasks []Task, maxConcurrent int) {
sem := make(chan struct{}, maxConcurrent)
var wg sync.WaitGroup
for _, task := range tasks {
wg.Add(1)
sem <- struct{}{} // 슬롯 획득
go func(t Task) {
defer wg.Done()
defer func() { <-sem }() // 슬롯 반환
process(t)
}(task)
}
wg.Wait()
}
4.5 errgroup 패턴
import "golang.org/x/sync/errgroup"
func fetchAll(ctx context.Context, urls []string) ([]string, error) {
g, ctx := errgroup.WithContext(ctx)
results := make([]string, len(urls))
for i, url := range urls {
i, url := i, url // 루프 변수 캡처
g.Go(func() error {
body, err := fetchURL(ctx, url)
if err != nil {
return fmt.Errorf("fetching %s: %w", url, err)
}
results[i] = body
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err // 첫 번째 에러 반환, 나머지 취소
}
return results, nil
}
5. 에러 처리
5.1 기본 에러 패턴
Go는 예외(exception) 대신 값으로 에러를 반환합니다:
// 기본 에러 반환
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
// fmt.Errorf로 컨텍스트 추가
func readConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("reading config %s: %w", path, err)
}
// ...
return config, nil
}
5.2 Sentinel 에러와 커스텀 에러
// Sentinel 에러 (패키지 레벨 변수)
var (
ErrNotFound = errors.New("not found")
ErrUnauthorized = errors.New("unauthorized")
ErrConflict = errors.New("conflict")
)
// 커스텀 에러 타입
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation error: %s - %s", e.Field, e.Message)
}
// errors.Is / errors.As 사용
func handleError(err error) {
// Sentinel 에러 비교
if errors.Is(err, ErrNotFound) {
// 404 처리
}
// 커스텀 에러 타입 추출
var valErr *ValidationError
if errors.As(err, &valErr) {
fmt.Printf("Field: %s, Message: %s\n", valErr.Field, valErr.Message)
}
}
5.3 Defer, Panic, Recover
// defer: 함수 종료 시 역순 실행 (리소스 정리에 핵심)
func readFile(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close() // 함수 종료 시 반드시 실행
data, err := io.ReadAll(f)
return string(data), err
}
// panic/recover: 정말 복구 불가능한 상황에서만 사용
func safeExecute(fn func()) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered from panic: %v", r)
}
}()
fn()
return nil
}
6. 제네릭 (Go 1.18+)
6.1 기본 문법
// 제네릭 함수
func Map[T any, U any](slice []T, fn func(T) U) []U {
result := make([]U, len(slice))
for i, v := range slice {
result[i] = fn(v)
}
return result
}
func Filter[T any](slice []T, predicate func(T) bool) []T {
var result []T
for _, v := range slice {
if predicate(v) {
result = append(result, v)
}
}
return result
}
// 사용
nums := []int{1, 2, 3, 4, 5}
doubled := Map(nums, func(n int) int { return n * 2 })
evens := Filter(nums, func(n int) bool { return n%2 == 0 })
6.2 타입 제약 (Constraints)
// 커스텀 제약
type Number interface {
~int | ~int32 | ~int64 | ~float32 | ~float64
}
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
// comparable 제약 (맵 키로 사용 가능한 타입)
func Contains[T comparable](slice []T, target T) bool {
for _, v := range slice {
if v == target {
return true
}
}
return false
}
// 제네릭 구조체
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(item T) {
s.items = append(s.items, item)
}
func (s *Stack[T]) Pop() (T, bool) {
if len(s.items) == 0 {
var zero T
return zero, false
}
item := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return item, true
}
6.3 제네릭 사용 가이드라인
제네릭 사용 판단 기준:
┌─────────────────────────────────────────────────────────┐
│ 사용하면 좋은 경우 │ 피해야 하는 경우 │
│ ─────────────────────────────── │ ────────────────────── │
│ 컬렉션 유틸리티 (Map, Filter) │ 단 2-3개 타입만 지원 │
│ 자료구조 (Stack, Queue, Tree) │ 인터페이스로 충분할 때 │
│ 타입 안전한 결과/옵션 타입 │ 코드 가독성이 떨어질 때 │
│ 정렬, 검색 알고리즘 │ 리플렉션이 필요한 경우 │
└─────────────────────────────────────────────────────────┘
7. 테스트
7.1 테이블 주도 테스트 (Table-Driven Tests)
Go의 관용적 테스트 패턴:
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive numbers", 2, 3, 5},
{"negative numbers", -1, -2, -3},
{"zero", 0, 0, 0},
{"mixed", -1, 5, 4},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Add(tt.a, tt.b)
if result != tt.expected {
t.Errorf("Add(%d, %d) = %d, want %d",
tt.a, tt.b, result, tt.expected)
}
})
}
}
7.2 Testify 라이브러리
import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestUser(t *testing.T) {
user, err := CreateUser("John", "john@example.com")
require.NoError(t, err) // 에러 시 즉시 중단
assert.Equal(t, "John", user.Name) // 에러 시 계속 진행
assert.NotEmpty(t, user.ID)
assert.WithinDuration(t, time.Now(), user.CreatedAt, time.Second)
}
7.3 HTTP 테스트
func TestGetUserHandler(t *testing.T) {
// 테스트 서버 설정
handler := http.HandlerFunc(GetUserHandler)
req := httptest.NewRequest("GET", "/users/123", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
var user User
err := json.NewDecoder(rec.Body).Decode(&user)
require.NoError(t, err)
assert.Equal(t, "123", user.ID)
}
7.4 벤치마크와 퍼징
// 벤치마크
func BenchmarkFibonacci(b *testing.B) {
for i := 0; i < b.N; i++ {
Fibonacci(20)
}
}
// 퍼징 (Go 1.18+)
func FuzzParseJSON(f *testing.F) {
// 시드 코퍼스
f.Add([]byte(`{"name": "test"}`))
f.Add([]byte(`{}`))
f.Add([]byte(`[]`))
f.Fuzz(func(t *testing.T, data []byte) {
var result map[string]interface{}
if err := json.Unmarshal(data, &result); err != nil {
return // 유효하지 않은 JSON은 무시
}
// 다시 마샬링하면 에러 없어야 함
_, err := json.Marshal(result)
if err != nil {
t.Errorf("re-marshal failed: %v", err)
}
})
}
# 테스트 실행
go test ./... # 전체 테스트
go test -v -run TestUser ./pkg/user # 특정 테스트
go test -bench=. ./... # 벤치마크
go test -fuzz=FuzzParseJSON ./... # 퍼징
go test -race ./... # 레이스 디텍터
go test -cover ./... # 커버리지
8. REST API 구축
8.1 Chi 라우터 + 미들웨어
package main
import (
"encoding/json"
"log"
"net/http"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
type Server struct {
router *chi.Mux
db *sql.DB
}
func NewServer(db *sql.DB) *Server {
s := &Server{
router: chi.NewRouter(),
db: db,
}
s.routes()
return s
}
func (s *Server) routes() {
// 미들웨어
s.router.Use(middleware.Logger)
s.router.Use(middleware.Recoverer)
s.router.Use(middleware.Timeout(30 * time.Second))
s.router.Use(corsMiddleware)
// 라우트
s.router.Route("/api/v1", func(r chi.Router) {
r.Get("/health", s.handleHealth)
r.Route("/users", func(r chi.Router) {
r.Get("/", s.handleListUsers)
r.Post("/", s.handleCreateUser)
r.Route("/{userID}", func(r chi.Router) {
r.Get("/", s.handleGetUser)
r.Put("/", s.handleUpdateUser)
r.Delete("/", s.handleDeleteUser)
})
})
})
}
func (s *Server) handleCreateUser(w http.ResponseWriter, r *http.Request) {
var input CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
respondError(w, http.StatusBadRequest, "invalid request body")
return
}
user, err := s.userService.Create(r.Context(), input)
if err != nil {
respondError(w, http.StatusInternalServerError, err.Error())
return
}
respondJSON(w, http.StatusCreated, user)
}
// 헬퍼 함수
func respondJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func respondError(w http.ResponseWriter, status int, message string) {
respondJSON(w, status, map[string]string{"error": message})
}
8.2 데이터베이스 (sqlx)
import "github.com/jmoiron/sqlx"
type UserRepository struct {
db *sqlx.DB
}
func (r *UserRepository) GetByID(ctx context.Context, id string) (*User, error) {
var user User
err := r.db.GetContext(ctx, &user,
"SELECT id, name, email, created_at FROM users WHERE id = $1", id)
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
return &user, err
}
func (r *UserRepository) List(ctx context.Context, limit, offset int) ([]User, error) {
var users []User
err := r.db.SelectContext(ctx, &users,
"SELECT id, name, email, created_at FROM users ORDER BY created_at DESC LIMIT $1 OFFSET $2",
limit, offset)
return users, err
}
9. gRPC 서비스 구축
9.1 Protocol Buffers 정의
// proto/user/v1/user.proto
syntax = "proto3";
package user.v1;
option go_package = "gen/user/v1;userv1";
service UserService {
rpc GetUser(GetUserRequest) returns (GetUserResponse);
rpc ListUsers(ListUsersRequest) returns (ListUsersResponse);
rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
rpc StreamUpdates(StreamUpdatesRequest) returns (stream UserEvent);
}
message User {
string id = 1;
string name = 2;
string email = 3;
int64 created_at = 4;
}
message GetUserRequest {
string id = 1;
}
message GetUserResponse {
User user = 1;
}
9.2 gRPC 서버 구현
type userServer struct {
userv1.UnimplementedUserServiceServer
repo UserRepository
}
func (s *userServer) GetUser(ctx context.Context, req *userv1.GetUserRequest) (*userv1.GetUserResponse, error) {
user, err := s.repo.GetByID(ctx, req.Id)
if err != nil {
if errors.Is(err, ErrNotFound) {
return nil, status.Error(codes.NotFound, "user not found")
}
return nil, status.Error(codes.Internal, "internal error")
}
return &userv1.GetUserResponse{User: toProtoUser(user)}, nil
}
// 서버 스트리밍
func (s *userServer) StreamUpdates(req *userv1.StreamUpdatesRequest, stream userv1.UserService_StreamUpdatesServer) error {
for event := range s.eventCh {
if err := stream.Send(event); err != nil {
return err
}
}
return nil
}
// 인터셉터 (미들웨어)
func loggingInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
start := time.Now()
resp, err := handler(ctx, req)
log.Printf("method=%s duration=%s error=%v", info.FullMethod, time.Since(start), err)
return resp, err
}
10. CLI 도구 개발
10.1 Cobra + Viper
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var rootCmd = &cobra.Command{
Use: "mytool",
Short: "A powerful CLI tool",
Long: "mytool is a CLI application for managing deployments",
}
var deployCmd = &cobra.Command{
Use: "deploy [environment]",
Short: "Deploy application",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
env := args[0]
image, _ := cmd.Flags().GetString("image")
replicas, _ := cmd.Flags().GetInt("replicas")
dryRun, _ := cmd.Flags().GetBool("dry-run")
fmt.Printf("Deploying to %s: image=%s, replicas=%d, dry-run=%v\n",
env, image, replicas, dryRun)
if dryRun {
fmt.Println("Dry run mode - no changes applied")
return nil
}
return executeDeploy(env, image, replicas)
},
}
func init() {
// 플래그 설정
deployCmd.Flags().StringP("image", "i", "", "Container image (required)")
deployCmd.Flags().IntP("replicas", "r", 1, "Number of replicas")
deployCmd.Flags().Bool("dry-run", false, "Dry run mode")
deployCmd.MarkFlagRequired("image")
// Viper로 설정 파일 바인딩
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath(".")
viper.AutomaticEnv()
rootCmd.AddCommand(deployCmd)
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}
10.2 BubbleTea TUI 예제
import (
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
type model struct {
choices []string
cursor int
selected map[int]struct{}
}
func (m model) Init() tea.Cmd { return nil }
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "q", "ctrl+c":
return m, tea.Quit
case "up", "k":
if m.cursor > 0 {
m.cursor--
}
case "down", "j":
if m.cursor < len(m.choices)-1 {
m.cursor++
}
case "enter", " ":
if _, ok := m.selected[m.cursor]; ok {
delete(m.selected, m.cursor)
} else {
m.selected[m.cursor] = struct{}{}
}
}
}
return m, nil
}
func (m model) View() string {
s := "Select items:\n\n"
for i, choice := range m.choices {
cursor := " "
if m.cursor == i {
cursor = ">"
}
checked := " "
if _, ok := m.selected[i]; ok {
checked = "x"
}
s += fmt.Sprintf("%s [%s] %s\n", cursor, checked, choice)
}
s += "\nPress q to quit.\n"
return s
}
11. 프로덕션 배포
11.1 Docker 멀티스테이지 빌드 (5MB 바이너리)
# 빌드 스테이지
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -ldflags="-s -w" -o /app/server ./cmd/server
# 실행 스테이지 (scratch = 빈 이미지)
FROM scratch
# TLS 인증서 (HTTPS 호출 필요 시)
COPY /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# 바이너리만 복사
COPY /app/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]
# 빌드 결과 크기 비교
# Go scratch: ~5-10MB
# Go alpine: ~15MB
# Node.js: ~200MB+
# Java Spring: ~300MB+
# Python: ~150MB+
11.2 Graceful Shutdown
func main() {
srv := &http.Server{
Addr: ":8080",
Handler: router,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
// 서버 시작 (별도 고루틴)
go func() {
log.Printf("Server starting on :8080")
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("Server error: %v", err)
}
}()
// 시그널 대기 (SIGINT, SIGTERM)
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down gracefully...")
// 30초 타임아웃으로 기존 요청 완료 대기
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatalf("Forced shutdown: %v", err)
}
log.Println("Server stopped")
}
11.3 pprof 프로파일링
import _ "net/http/pprof"
func main() {
// pprof 엔드포인트 노출 (별도 포트 권장)
go func() {
log.Println(http.ListenAndServe(":6060", nil))
}()
// 메인 서버 실행
// ...
}
# CPU 프로파일
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
# 메모리 프로파일
go tool pprof http://localhost:6060/debug/pprof/heap
# 고루틴 프로파일
go tool pprof http://localhost:6060/debug/pprof/goroutine
# 웹 UI
go tool pprof -http=:8081 profile.out
11.4 레이스 디텍터
# 테스트 시 레이스 감지
go test -race ./...
# 빌드 시 레이스 감지 포함
go build -race -o server ./cmd/server
12. Go vs Rust vs Java 비교
┌─────────────────┬──────────────────┬──────────────────┬──────────────────┐
│ 항목 │ Go │ Rust │ Java │
├─────────────────┼──────────────────┼──────────────────┼──────────────────┤
│ 학습 곡선 │ 낮음 (1-2주) │ 높음 (3-6개월) │ 중간 (1-2개월) │
│ 컴파일 속도 │ 매우 빠름 │ 느림 │ 보통 │
│ 실행 속도 │ 빠름 │ 매우 빠름 │ 빠름 (JIT) │
│ 메모리 관리 │ GC │ 소유권 시스템 │ GC │
│ 동시성 │ 고루틴/채널 │ async/tokio │ Virtual Thread │
│ 바이너리 크기 │ 5-15MB │ 1-5MB │ 100MB+ (JRE) │
│ 생태계 │ 클라우드/인프라 │ 시스템/WebAssembly│ 엔터프라이즈 │
│ 에러 처리 │ 값 반환 │ Result/Option │ 예외(Exception) │
│ 제네릭 │ 1.18+ (기본) │ 강력함 │ 완전 지원 │
│ 주요 사용처 │ 마이크로서비스, │ OS, 게임 엔진, │ 대규모 엔터프 │
│ │ CLI, DevOps │ 임베디드 │ 라이즈, 안드로이드│
│ 기업 채택 │ Google, Uber │ Mozilla, AWS │ 대부분의 대기업 │
│ 연봉 (US) │ 높음 (상위 5) │ 매우 높음 (1위) │ 보통 │
└─────────────────┴──────────────────┴──────────────────┴──────────────────┘
Go가 적합한 경우: 마이크로서비스, API 서버, CLI 도구, DevOps 도구, 네트워크 프로그래밍 Rust가 적합한 경우: 시스템 프로그래밍, 게임 엔진, 임베디드, 최대 성능이 필요한 경우 Java가 적합한 경우: 대규모 엔터프라이즈, 안드로이드 앱, 기존 레거시 시스템
13. 면접 질문 15선
기초
-
Go에서 슬라이스와 배열의 차이는? 배열은 고정 크기로 타입의 일부이고(예:
[5]int), 슬라이스는 동적 크기의 배열 뷰입니다. 슬라이스는 내부적으로 포인터, 길이, 용량을 가집니다. -
Go에 상속이 없는 이유는? 어떻게 코드를 재사용하나요? Go는 상속 대신 구조체 임베딩(컴포지션)을 사용합니다. "상속보다 컴포지션" 원칙을 언어 레벨에서 강제합니다.
-
인터페이스가 nil인 경우와 nil 포인터를 가진 인터페이스의 차이는? 인터페이스는 (type, value) 쌍입니다. 둘 다 nil이면 nil 인터페이스이고, type은 있지만 value가 nil이면 non-nil 인터페이스입니다.
-
defer의 실행 순서와 주의사항은? LIFO(후입선출) 순서로 실행됩니다. defer 시점의 인자 값이 캡처되므로, 루프에서 defer 사용 시 주의가 필요합니다.
-
Go의 제로 값(zero value) 철학은? 모든 변수가 선언 시 유효한 기본값을 가지므로 초기화되지 않은 변수 버그를 방지합니다. 이로 인해 생성자가 덜 필요합니다.
동시성
-
고루틴이 OS 스레드보다 가벼운 이유는? 고루틴은 약 2KB 스택으로 시작하고(OS 스레드는 1MB+), Go 런타임의 M:N 스케줄러가 수천 개 고루틴을 소수 OS 스레드에 매핑합니다.
-
버퍼 채널과 언버퍼 채널의 차이는? 언버퍼 채널은 송신과 수신이 동기적으로 만나야 합니다. 버퍼 채널은 용량만큼 비동기 전송 가능합니다.
-
고루틴 누수(leak)가 발생하는 상황과 방지법은? 채널에 전송/수신할 상대가 없거나, context 취소를 처리하지 않으면 발생합니다. context.WithCancel/Timeout 사용이 핵심입니다.
-
데드락이 발생하는 조건과 Go에서의 감지 방법은? Go 런타임이 모든 고루틴이 블록되면 감지하여 panic합니다. go vet과 race detector도 도움됩니다.
-
sync.Mutex vs sync.RWMutex 사용 기준은? 읽기가 쓰기보다 훨씬 많으면 RWMutex가 유리합니다. 여러 고루틴이 동시에 읽기 가능하기 때문입니다.
고급
-
context.Context의 주요 용도 3가지는? 취소 전파(Cancel), 타임아웃(Timeout/Deadline), 값 전달(WithValue). HTTP 핸들러와 DB 호출에 항상 사용해야 합니다.
-
Go의 GC(Garbage Collector) 특성은? Go는 동시 마크앤스윕(concurrent mark-and-sweep) GC를 사용하며, STW(stop-the-world) 시간이 1ms 미만입니다. GOGC 환경변수로 조절 가능합니다.
-
제네릭 도입 전후의 코드 차이를 설명하세요. 도입 전에는 interface와 타입 단언(type assertion) 또는 코드 생성이 필요했습니다. 도입 후에는 타입 파라미터로 컴파일 타임 타입 안전성을 제공합니다.
-
Go에서 의존성 주입(DI)은 어떻게 하나요? 프레임워크 없이 생성자 함수를 통해 인터페이스를 주입합니다. wire(Google), fx(Uber) 같은 DI 프레임워크도 있습니다.
-
Go 모듈 시스템에서 indirect 의존성 관리는? go.mod의 indirect 주석은 직접 임포트하지 않는 의존성입니다. go mod tidy로 정리하고, go mod vendor로 벤더링할 수 있습니다.
14. 실전 퀴즈
Q1: 다음 코드의 출력은?
func main() {
for i := 0; i < 3; i++ {
go func() {
fmt.Println(i)
}()
}
time.Sleep(time.Second)
}
답: Go 1.22 이전에는 대부분 3, 3, 3이 출력됩니다. 클로저가 루프 변수 i를 참조하는데, 고루틴이 실행될 때 이미 루프가 종료되어 i=3입니다. Go 1.22부터는 루프 변수가 각 반복에서 새로 생성되어 0, 1, 2(순서 불확정)가 출력됩니다.
Q2: 이 코드에서 데드락이 발생하는 이유는?
func main() {
ch := make(chan int)
ch <- 42
fmt.Println(<-ch)
}
답: 언버퍼 채널에 전송하면 수신자가 나타날 때까지 블록됩니다. 메인 고루틴이 ch <- 42에서 블록되어 <-ch 수신에 도달할 수 없으므로 데드락입니다. 해결: 별도 고루틴에서 전송하거나 버퍼 채널 사용.
Q3: nil 인터페이스 문제 - 왜 err != nil이 true인가?
type MyError struct{}
func (e *MyError) Error() string { return "error" }
func doSomething() error {
var err *MyError = nil
return err
}
func main() {
err := doSomething()
fmt.Println(err == nil) // false!
}
답: doSomething()은 nil 포인터를 가진 *MyError 타입의 인터페이스를 반환합니다. 인터페이스는 (type=MyError, value=nil)이므로 nil이 아닙니다. 해결: 함수에서 명시적으로 return nil을 반환합니다.
Q4: WaitGroup 사용 시 흔한 실수는?
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
go func(id int) {
wg.Add(1) // 잘못된 위치!
defer wg.Done()
fmt.Println(id)
}(i)
}
wg.Wait()
}
답: wg.Add(1)이 고루틴 안에 있으므로 wg.Wait()가 먼저 실행될 수 있습니다. 올바른 방법은 고루틴 시작 전에 wg.Add(1)을 호출하는 것입니다.
Q5: context 취소가 전파되는 원리를 설명하세요.
답: context.WithCancel은 부모 컨텍스트의 Done 채널을 감시하는 자식 컨텍스트를 생성합니다. 부모가 취소되면 자식의 Done 채널도 닫힙니다. 이 체인은 최상위까지 전파되어, 하나의 취소로 전체 요청 트리를 정리할 수 있습니다. select 문에서 ctx.Done()을 확인하여 고루틴이 적절히 종료해야 합니다.
참고 자료
- Go 공식 문서
- Effective Go
- Go by Example
- Go Concurrency Patterns (Rob Pike)
- Advanced Go Concurrency Patterns
- The Go Programming Language (Donovan & Kernighan)
- Concurrency in Go (Katherine Cox-Buday)
- Go Wiki: Table Driven Tests
- Go Wiki: Code Review Comments
- Uber Go Style Guide
- Go Module Reference
- Go Generics Tutorial
- pprof 가이드
- Chi Router
- gRPC Go 공식 가이드
- Cobra CLI 라이브러리
- BubbleTea TUI 프레임워크