Skip to content

Split View: Go 프로그래밍 완전 가이드 — 고루틴, 채널, 인터페이스, 실전 패턴

|

Go 프로그래밍 완전 가이드 — 고루틴, 채널, 인터페이스, 실전 패턴

1. 왜 Go인가

Go(Golang)는 2009년 Google에서 만든 언어입니다. 설계 철학은 단순함, 빠른 컴파일, 강력한 동시성입니다.

Go가 선택받는 이유

  • Docker, Kubernetes, Terraform, Prometheus 등 클라우드 인프라의 핵심 도구가 모두 Go로 작성되었습니다
  • 컴파일 속도가 매우 빠릅니다. 수십만 줄 프로젝트도 몇 초 안에 빌드됩니다
  • 정적 바이너리를 생성하므로 배포가 단순합니다. 별도 런타임이 필요 없습니다
  • 고루틴을 통해 수십만 개의 동시 작업을 효율적으로 처리합니다
  • 가비지 컬렉션이 있지만, 지연 시간이 매우 짧습니다

Go vs 다른 언어

항목GoPythonJavaRust
컴파일 속도매우 빠름인터프리터느림느림
실행 속도빠름느림빠름매우 빠름
동시성고루틴asyncio스레드async/tokio
학습 곡선낮음매우 낮음높음매우 높음
메모리 관리GCGCGC소유권

Go는 생산성과 성능의 최적 균형점에 위치합니다. Rust만큼 빠르진 않지만, 팀 전체가 빠르게 배우고 유지보수할 수 있습니다.


2. 기본 문법

변수와 타입

package main

import "fmt"

func main() {
    // 명시적 선언
    var name string = "Gopher"
    var age int = 10

    // 짧은 선언 (타입 추론)
    language := "Go"
    version := 1.22

    // 상수
    const pi = 3.14159

    fmt.Printf("%s는 %d살이고 %s %.2f를 씁니다\n",
        name, age, language, version)
    fmt.Println("원주율:", pi)
}

함수

// 기본 함수
func add(a, b int) int {
    return a + b
}

// 다중 반환값
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("0으로 나눌 수 없습니다")
    }
    return a / b, nil
}

// 명명된 반환값
func swap(a, b string) (first, second string) {
    first = b
    second = a
    return // naked return
}

// 가변 인자
func sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}

구조체

type User struct {
    Name  string
    Email string
    Age   int
}

// 메서드 (값 리시버)
func (u User) String() string {
    return fmt.Sprintf("%s (%s)", u.Name, u.Email)
}

// 메서드 (포인터 리시버 - 값 수정 가능)
func (u *User) SetEmail(email string) {
    u.Email = email
}

func main() {
    user := User{Name: "김영주", Email: "yj@example.com", Age: 30}
    user.SetEmail("new@example.com")
    fmt.Println(user) // 김영주 (new@example.com)
}

포인터

func main() {
    x := 42
    p := &x    // x의 주소
    fmt.Println(*p) // 42 (역참조)
    *p = 100
    fmt.Println(x) // 100
}

// Go에는 포인터 산술이 없습니다 - 안전합니다
// C와 달리 댕글링 포인터 걱정이 적습니다

슬라이스와 맵

func main() {
    // 슬라이스
    nums := []int{1, 2, 3, 4, 5}
    nums = append(nums, 6, 7)
    sub := nums[1:4] // [2, 3, 4]

    // make로 생성
    buffer := make([]byte, 0, 1024) // len=0, cap=1024

    // 맵
    scores := map[string]int{
        "Alice": 95,
        "Bob":   87,
    }
    scores["Charlie"] = 92

    // 키 존재 확인
    val, ok := scores["Dave"]
    if !ok {
        fmt.Println("Dave의 점수가 없습니다")
    }

    // 맵 순회
    for name, score := range scores {
        fmt.Printf("%s: %d\n", name, score)
    }
}

3. 고루틴과 채널

Go의 동시성 모델은 CSP(Communicating Sequential Processes) 기반입니다. 핵심은 두 가지입니다: 고루틴채널.

고루틴 기초

func worker(id int) {
    fmt.Printf("Worker %d 시작\n", id)
    time.Sleep(time.Second)
    fmt.Printf("Worker %d 완료\n", id)
}

func main() {
    for i := 1; i <= 5; i++ {
        go worker(i) // 고루틴 실행
    }

    // 메인 고루틴이 끝나면 프로그램 종료
    time.Sleep(2 * time.Second)
}

고루틴은 OS 스레드가 아닙니다. Go 런타임이 수천 개의 고루틴을 소수의 OS 스레드에 다중화(multiplexing) 합니다. 고루틴 하나의 초기 스택은 약 2KB로, 수십만 개를 동시에 실행할 수 있습니다.

채널 통신

func producer(ch chan<- int) {
    for i := 0; i < 5; i++ {
        ch <- i // 채널에 값 전송
        fmt.Printf("전송: %d\n", i)
    }
    close(ch) // 채널 닫기
}

func consumer(ch <-chan int) {
    for val := range ch { // 채널이 닫힐 때까지 수신
        fmt.Printf("수신: %d\n", val)
    }
}

func main() {
    ch := make(chan int, 3) // 버퍼 크기 3

    go producer(ch)
    consumer(ch) // 메인 고루틴에서 소비
}

select 문

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
        time.Sleep(1 * time.Second)
        ch1 <- "one"
    }()

    go func() {
        time.Sleep(2 * time.Second)
        ch2 <- "two"
    }()

    for i := 0; i < 2; i++ {
        select {
        case msg := <-ch1:
            fmt.Println("ch1:", msg)
        case msg := <-ch2:
            fmt.Println("ch2:", msg)
        case <-time.After(3 * time.Second):
            fmt.Println("타임아웃!")
        }
    }
}

WaitGroup

func main() {
    var wg sync.WaitGroup

    urls := []string{
        "https://example.com",
        "https://golang.org",
        "https://github.com",
    }

    for _, url := range urls {
        wg.Add(1)
        go func(u string) {
            defer wg.Done()
            resp, err := http.Get(u)
            if err != nil {
                fmt.Printf("에러: %s - %v\n", u, err)
                return
            }
            defer resp.Body.Close()
            fmt.Printf("%s -> %s\n", u, resp.Status)
        }(url)
    }

    wg.Wait() // 모든 고루틴 완료 대기
    fmt.Println("모든 요청 완료")
}

데드락 방지 패턴

// 나쁜 예: 데드락 발생
func bad() {
    ch := make(chan int) // 버퍼 없는 채널
    ch <- 1             // 수신자가 없어서 영원히 블록
    fmt.Println(<-ch)
}

// 좋은 예 1: 고루틴으로 전송
func good1() {
    ch := make(chan int)
    go func() { ch <- 1 }()
    fmt.Println(<-ch)
}

// 좋은 예 2: 버퍼 채널 사용
func good2() {
    ch := make(chan int, 1)
    ch <- 1
    fmt.Println(<-ch)
}

워커 풀 패턴

func workerPool(jobs <-chan int, results chan<- int, id int) {
    for j := range jobs {
        fmt.Printf("Worker %d 처리 중: job %d\n", id, j)
        time.Sleep(time.Millisecond * 500)
        results <- j * 2
    }
}

func main() {
    const numJobs = 10
    const numWorkers = 3

    jobs := make(chan int, numJobs)
    results := make(chan int, numJobs)

    // 워커 시작
    for w := 1; w <= numWorkers; w++ {
        go workerPool(jobs, results, w)
    }

    // 작업 전송
    for j := 1; j <= numJobs; j++ {
        jobs <- j
    }
    close(jobs)

    // 결과 수집
    for r := 1; r <= numJobs; r++ {
        result := <-results
        fmt.Printf("결과: %d\n", result)
    }
}

4. 인터페이스

Go의 인터페이스는 암시적으로 구현됩니다. implements 키워드가 없습니다.

기본 인터페이스

type Shape interface {
    Area() float64
    Perimeter() float64
}

type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

func (c Circle) Perimeter() float64 {
    return 2 * math.Pi * c.Radius
}

type Rectangle struct {
    Width, Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

// Shape 인터페이스를 매개변수로 받는 함수
func printInfo(s Shape) {
    fmt.Printf("면적: %.2f, 둘레: %.2f\n", s.Area(), s.Perimeter())
}

io.Reader / io.Writer

Go 표준 라이브러리의 가장 강력한 인터페이스입니다.

// io.Reader 인터페이스
// type Reader interface {
//     Read(p []byte) (n int, err error)
// }

// io.Writer 인터페이스
// type Writer interface {
//     Write(p []byte) (n int, err error)
// }

func countBytes(r io.Reader) (int, error) {
    buf := make([]byte, 1024)
    total := 0
    for {
        n, err := r.Read(buf)
        total += n
        if err == io.EOF {
            return total, nil
        }
        if err != nil {
            return total, err
        }
    }
}

func main() {
    // 문자열에서 읽기
    r := strings.NewReader("Hello, Go!")
    n, _ := countBytes(r)
    fmt.Printf("%d 바이트\n", n) // 10 바이트

    // 파일에서 읽기
    f, _ := os.Open("data.txt")
    defer f.Close()
    n, _ = countBytes(f)
    fmt.Printf("%d 바이트\n", n)
}

빈 인터페이스와 타입 단언

func describe(i interface{}) {
    // 타입 단언
    if s, ok := i.(string); ok {
        fmt.Println("문자열:", s)
        return
    }

    // 타입 스위치
    switch v := i.(type) {
    case int:
        fmt.Println("정수:", v)
    case float64:
        fmt.Println("실수:", v)
    case bool:
        fmt.Println("불린:", v)
    default:
        fmt.Printf("알 수 없는 타입: %T\n", v)
    }
}

func main() {
    describe(42)
    describe("hello")
    describe(3.14)
    describe(true)
}

인터페이스 조합

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

type Closer interface {
    Close() error
}

// 인터페이스 조합 (embedding)
type ReadWriter interface {
    Reader
    Writer
}

type ReadWriteCloser interface {
    Reader
    Writer
    Closer
}

5. 에러 처리

Go는 예외(exception) 대신 명시적 에러 반환을 사용합니다.

errors.New와 fmt.Errorf

import (
    "errors"
    "fmt"
)

var ErrNotFound = errors.New("not found")

func findUser(id int) (*User, error) {
    if id <= 0 {
        return nil, fmt.Errorf("잘못된 사용자 ID: %d", id)
    }
    // ... DB 조회
    return nil, ErrNotFound
}

에러 래핑과 errors.Is / errors.As

type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("유효성 검사 실패 - %s: %s", e.Field, e.Message)
}

func validateAge(age int) error {
    if age < 0 || age > 150 {
        return &ValidationError{
            Field:   "age",
            Message: fmt.Sprintf("나이는 0-150 범위여야 합니다 (입력값: %d)", age),
        }
    }
    return nil
}

func processUser(age int) error {
    if err := validateAge(age); err != nil {
        return fmt.Errorf("사용자 처리 실패: %w", err) // 에러 래핑
    }
    return nil
}

func main() {
    err := processUser(-5)

    // errors.Is: 에러 체인에서 특정 에러 확인
    if errors.Is(err, ErrNotFound) {
        fmt.Println("사용자를 찾을 수 없습니다")
    }

    // errors.As: 에러 체인에서 특정 타입으로 변환
    var valErr *ValidationError
    if errors.As(err, &valErr) {
        fmt.Printf("필드: %s, 메시지: %s\n", valErr.Field, valErr.Message)
    }
}

커스텀 에러 패턴

type AppError struct {
    Code    int
    Message string
    Err     error
}

func (e *AppError) Error() string {
    if e.Err != nil {
        return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Err)
    }
    return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}

func (e *AppError) Unwrap() error {
    return e.Err
}

// 에러 생성 헬퍼
func NewAppError(code int, msg string, err error) *AppError {
    return &AppError{Code: code, Message: msg, Err: err}
}

6. 제네릭 (Go 1.18+)

Go 1.18부터 제네릭이 도입되어 타입 안전한 범용 코드를 작성할 수 있습니다.

타입 파라미터

func Min[T constraints.Ordered](a, b T) T {
    if a < b {
        return a
    }
    return b
}

func main() {
    fmt.Println(Min(3, 5))       // int
    fmt.Println(Min(3.14, 2.71)) // float64
    fmt.Println(Min("a", "b"))   // string
}

제약 조건

import "golang.org/x/exp/constraints"

// 커스텀 제약 조건
type Number interface {
    constraints.Integer | constraints.Float
}

func Sum[T Number](nums []T) T {
    var total T
    for _, n := range nums {
        total += n
    }
    return total
}

// 구조체에 제네릭 적용
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
}

func (s *Stack[T]) Len() int {
    return len(s.items)
}

실전 제네릭 예제: 맵 유틸리티

func Keys[K comparable, V any](m map[K]V) []K {
    keys := make([]K, 0, len(m))
    for k := range m {
        keys = append(keys, k)
    }
    return keys
}

func Values[K comparable, V any](m map[K]V) []V {
    vals := make([]V, 0, len(m))
    for _, v := range m {
        vals = append(vals, v)
    }
    return vals
}

func Filter[T any](slice []T, predicate func(T) bool) []T {
    var result []T
    for _, item := range slice {
        if predicate(item) {
            result = append(result, item)
        }
    }
    return result
}

func Map[T any, U any](slice []T, transform func(T) U) []U {
    result := make([]U, len(slice))
    for i, item := range slice {
        result[i] = transform(item)
    }
    return result
}

7. 테스트

Go는 내장 테스트 프레임워크가 강력합니다. 별도 라이브러리 없이도 충분합니다.

기본 테스트

// math.go
package math

func Add(a, b int) int {
    return a + b
}

func Fibonacci(n int) int {
    if n <= 1 {
        return n
    }
    return Fibonacci(n-1) + Fibonacci(n-2)
}
// math_test.go
package math

import "testing"

func TestAdd(t *testing.T) {
    result := Add(2, 3)
    if result != 5 {
        t.Errorf("Add(2, 3) = %d; want 5", result)
    }
}

테이블 드리븐 테스트

func TestFibonacci(t *testing.T) {
    tests := []struct {
        name     string
        input    int
        expected int
    }{
        {"zero", 0, 0},
        {"one", 1, 1},
        {"two", 2, 1},
        {"five", 5, 5},
        {"ten", 10, 55},
    }

    for _, tc := range tests {
        t.Run(tc.name, func(t *testing.T) {
            result := Fibonacci(tc.input)
            if result != tc.expected {
                t.Errorf("Fibonacci(%d) = %d; want %d",
                    tc.input, result, tc.expected)
            }
        })
    }
}

벤치마크

func BenchmarkFibonacci(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Fibonacci(20)
    }
}

// 실행: go test -bench=. -benchmem
// BenchmarkFibonacci-8  28735  41523 ns/op  0 B/op  0 allocs/op

httptest

func TestHealthHandler(t *testing.T) {
    req := httptest.NewRequest("GET", "/health", nil)
    w := httptest.NewRecorder()

    healthHandler(w, req)

    resp := w.Result()
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        t.Errorf("상태 코드 = %d; want %d",
            resp.StatusCode, http.StatusOK)
    }

    body, _ := io.ReadAll(resp.Body)
    if string(body) != `{"status":"ok"}` {
        t.Errorf("응답 = %s; want {\"status\":\"ok\"}", body)
    }
}

8. 패키지와 모듈

go mod 초기화

# 새 프로젝트 시작
mkdir myproject && cd myproject
go mod init github.com/username/myproject

# 의존성 추가
go get github.com/gin-gonic/gin@latest

# 사용하지 않는 의존성 정리
go mod tidy

# 의존성 다운로드
go mod download

프로젝트 구조

myproject/
  cmd/
    server/
      main.go          # 엔트리포인트
  internal/
    handler/
      user.go          # HTTP 핸들러
      user_test.go
    service/
      user.go          # 비즈니스 로직
    repository/
      user.go          # 데이터 접근
  pkg/
    validator/
      validator.go     # 외부 공개 유틸리티
  go.mod
  go.sum

internal 패키지

internal 디렉토리에 있는 패키지는 해당 모듈 외부에서 임포트할 수 없습니다. 이를 통해 공개 API와 내부 구현을 명확히 분리합니다.

// internal/config/config.go
package config

type Config struct {
    Port     int
    DBHost   string
    LogLevel string
}

func Load() (*Config, error) {
    // 환경 변수에서 설정 로드
    return &Config{
        Port:     8080,
        DBHost:   "localhost:5432",
        LogLevel: "info",
    }, nil
}

9. 실전 패턴

웹 서버 (net/http)

package main

import (
    "encoding/json"
    "log"
    "net/http"
    "time"
)

type Response struct {
    Message   string `json:"message"`
    Timestamp string `json:"timestamp"`
}

func healthHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(Response{
        Message:   "ok",
        Timestamp: time.Now().Format(time.RFC3339),
    })
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /health", healthHandler)
    mux.HandleFunc("GET /api/users", getUsersHandler)

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

    log.Printf("서버 시작: %s", server.Addr)
    log.Fatal(server.ListenAndServe())
}

미들웨어

func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        log.Printf("[%s] %s 시작", r.Method, r.URL.Path)

        next.ServeHTTP(w, r)

        log.Printf("[%s] %s 완료 (%v)",
            r.Method, r.URL.Path, time.Since(start))
    })
}

func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        if token == "" {
            http.Error(w, "인증 필요", http.StatusUnauthorized)
            return
        }
        // 토큰 검증 로직...
        next.ServeHTTP(w, r)
    })
}

// 미들웨어 체이닝
func chain(h http.Handler, middlewares ...func(http.Handler) http.Handler) http.Handler {
    for i := len(middlewares) - 1; i >= 0; i-- {
        h = middlewares[i](h)
    }
    return h
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /health", healthHandler)

    handler := chain(mux, loggingMiddleware, authMiddleware)

    log.Fatal(http.ListenAndServe(":8080", handler))
}

Graceful Shutdown

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /health", healthHandler)

    server := &http.Server{
        Addr:    ":8080",
        Handler: mux,
    }

    // 서버를 고루틴에서 시작
    go func() {
        log.Println("서버 시작: :8080")
        if err := server.ListenAndServe(); err != http.ErrServerClosed {
            log.Fatalf("서버 에러: %v", err)
        }
    }()

    // 종료 시그널 대기
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit
    log.Println("서버 종료 시작...")

    // 30초 타임아웃으로 graceful shutdown
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    if err := server.Shutdown(ctx); err != nil {
        log.Fatalf("서버 강제 종료: %v", err)
    }

    log.Println("서버 종료 완료")
}

설정 관리

type Config struct {
    Server   ServerConfig
    Database DatabaseConfig
}

type ServerConfig struct {
    Port         int           `json:"port"`
    ReadTimeout  time.Duration `json:"read_timeout"`
    WriteTimeout time.Duration `json:"write_timeout"`
}

type DatabaseConfig struct {
    Host     string `json:"host"`
    Port     int    `json:"port"`
    User     string `json:"user"`
    Password string `json:"password"`
    DBName   string `json:"dbname"`
}

func (d DatabaseConfig) DSN() string {
    return fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
        d.Host, d.Port, d.User, d.Password, d.DBName)
}

func LoadConfig(path string) (*Config, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, fmt.Errorf("설정 파일 읽기 실패: %w", err)
    }

    var cfg Config
    if err := json.Unmarshal(data, &cfg); err != nil {
        return nil, fmt.Errorf("설정 파싱 실패: %w", err)
    }

    return &cfg, nil
}

10. Go in 2026

클라우드 인프라

Go는 클라우드 네이티브 생태계의 사실상 표준 언어입니다.

  • 컨테이너 오케스트레이션: Kubernetes, Docker, containerd
  • 서비스 메시: Istio, Linkerd
  • 모니터링: Prometheus, Grafana Agent, Thanos
  • IaC: Terraform, Pulumi
  • CI/CD: Drone, Tekton

CLI 도구

Go로 만든 CLI 도구는 단일 바이너리로 배포되어 설치가 간편합니다.

// cobra를 이용한 CLI 예제
package main

import (
    "fmt"
    "os"

    "github.com/spf13/cobra"
)

var rootCmd = &cobra.Command{
    Use:   "mytool",
    Short: "나의 CLI 도구",
}

var greetCmd = &cobra.Command{
    Use:   "greet [name]",
    Short: "인사하기",
    Args:  cobra.ExactArgs(1),
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Printf("안녕하세요, %s!\n", args[0])
    },
}

func main() {
    rootCmd.AddCommand(greetCmd)
    if err := rootCmd.Execute(); err != nil {
        os.Exit(1)
    }
}

마이크로서비스

Go는 마이크로서비스 아키텍처에 최적화되어 있습니다.

  • 빠른 시작 시간: 콜드 스타트 지연 최소화
  • 낮은 메모리 사용량: 컨테이너 리소스 절약
  • 정적 바이너리: 경량 Docker 이미지 (scratch 베이스 가능)
  • gRPC 네이티브 지원: protobuf + gRPC로 서비스 간 통신
  • OpenTelemetry: 분산 추적 및 메트릭 수집
# 멀티 스테이지 빌드
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /server ./cmd/server

FROM scratch
COPY --from=builder /server /server
EXPOSE 8080
ENTRYPOINT ["/server"]

최종 이미지 크기는 10-20MB 수준으로, Java나 Python 기반 서비스 대비 1/10 이하입니다.


마치며

Go는 단순함이 곧 강력함인 언어입니다. 복잡한 기능을 배제하고, 꼭 필요한 것만 제공합니다. 이 철학이 Docker, Kubernetes 같은 인프라 도구부터 CLI, 마이크로서비스까지 광범위한 영역에서 Go가 선택받는 이유입니다.

시작하기에 가장 좋은 방법은 직접 코드를 작성하는 것입니다. 간단한 CLI 도구나 REST API 서버를 만들어 보세요.

퀴즈: Go 동시성 이해도 점검

Q1. 고루틴과 OS 스레드의 차이는 무엇인가요?

Go 런타임이 고루틴을 소수의 OS 스레드에 다중화합니다. 고루틴의 초기 스택은 약 2KB로, OS 스레드의 1-8MB보다 훨씬 가볍습니다.

Q2. 버퍼 없는 채널에서 전송과 수신의 관계는?

전송자는 수신자가 준비될 때까지 블록되고, 수신자도 전송자가 준비될 때까지 블록됩니다. 동기적 통신 방식입니다.

Q3. select 문의 역할은?

여러 채널 연산을 동시에 대기하면서, 준비된 채널에서 먼저 처리합니다. 타임아웃이나 취소 처리에 유용합니다.

The Complete Go Programming Guide — Goroutines, Channels, Interfaces, and Practical Patterns

1. Why Go

Go (Golang) was created at Google in 2009. Its design philosophy centers on simplicity, fast compilation, and powerful concurrency.

Why Go Gets Chosen

  • Docker, Kubernetes, Terraform, Prometheus — the core tools of cloud infrastructure are all written in Go
  • Compilation speed is extremely fast. Projects with hundreds of thousands of lines build in seconds
  • It produces static binaries, making deployment simple. No separate runtime needed
  • Goroutines handle hundreds of thousands of concurrent tasks efficiently
  • Garbage collection exists but with very low latency

Go vs Other Languages

AspectGoPythonJavaRust
Compile SpeedVery FastInterpretedSlowSlow
Execution SpeedFastSlowFastVery Fast
ConcurrencyGoroutinesasyncioThreadsasync/tokio
Learning CurveLowVery LowHighVery High
Memory MgmtGCGCGCOwnership

Go sits at the optimal balance point between productivity and performance. It is not as fast as Rust, but entire teams can learn and maintain it quickly.


2. Basic Syntax

Variables and Types

package main

import "fmt"

func main() {
    // Explicit declaration
    var name string = "Gopher"
    var age int = 10

    // Short declaration (type inference)
    language := "Go"
    version := 1.22

    // Constants
    const pi = 3.14159

    fmt.Printf("%s is %d years old and uses %s %.2f\n",
        name, age, language, version)
    fmt.Println("Pi:", pi)
}

Functions

// Basic function
func add(a, b int) int {
    return a + b
}

// Multiple return values
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("cannot divide by zero")
    }
    return a / b, nil
}

// Named return values
func swap(a, b string) (first, second string) {
    first = b
    second = a
    return // naked return
}

// Variadic arguments
func sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}

Structs

type User struct {
    Name  string
    Email string
    Age   int
}

// Method (value receiver)
func (u User) String() string {
    return fmt.Sprintf("%s (%s)", u.Name, u.Email)
}

// Method (pointer receiver - can modify)
func (u *User) SetEmail(email string) {
    u.Email = email
}

func main() {
    user := User{Name: "Alice", Email: "alice@example.com", Age: 30}
    user.SetEmail("new@example.com")
    fmt.Println(user) // Alice (new@example.com)
}

Pointers

func main() {
    x := 42
    p := &x    // address of x
    fmt.Println(*p) // 42 (dereference)
    *p = 100
    fmt.Println(x) // 100
}

// Go has no pointer arithmetic - it is safe
// Unlike C, there is less worry about dangling pointers

Slices and Maps

func main() {
    // Slices
    nums := []int{1, 2, 3, 4, 5}
    nums = append(nums, 6, 7)
    sub := nums[1:4] // [2, 3, 4]

    // Create with make
    buffer := make([]byte, 0, 1024) // len=0, cap=1024

    // Maps
    scores := map[string]int{
        "Alice":   95,
        "Bob":     87,
    }
    scores["Charlie"] = 92

    // Check key existence
    val, ok := scores["Dave"]
    if !ok {
        fmt.Println("No score for Dave")
    }

    // Iterate map
    for name, score := range scores {
        fmt.Printf("%s: %d\n", name, score)
    }
}

3. Goroutines and Channels

Go's concurrency model is based on CSP (Communicating Sequential Processes). The two key primitives are goroutines and channels.

Goroutine Basics

func worker(id int) {
    fmt.Printf("Worker %d started\n", id)
    time.Sleep(time.Second)
    fmt.Printf("Worker %d done\n", id)
}

func main() {
    for i := 1; i <= 5; i++ {
        go worker(i) // launch goroutine
    }

    // When the main goroutine ends, the program exits
    time.Sleep(2 * time.Second)
}

Goroutines are not OS threads. The Go runtime multiplexes thousands of goroutines onto a small number of OS threads. Each goroutine starts with a stack of about 2KB, allowing hundreds of thousands to run concurrently.

Channel Communication

func producer(ch chan<- int) {
    for i := 0; i < 5; i++ {
        ch <- i // send value to channel
        fmt.Printf("Sent: %d\n", i)
    }
    close(ch) // close the channel
}

func consumer(ch <-chan int) {
    for val := range ch { // receive until channel is closed
        fmt.Printf("Received: %d\n", val)
    }
}

func main() {
    ch := make(chan int, 3) // buffered channel with capacity 3

    go producer(ch)
    consumer(ch) // consume in main goroutine
}

The select Statement

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
        time.Sleep(1 * time.Second)
        ch1 <- "one"
    }()

    go func() {
        time.Sleep(2 * time.Second)
        ch2 <- "two"
    }()

    for i := 0; i < 2; i++ {
        select {
        case msg := <-ch1:
            fmt.Println("ch1:", msg)
        case msg := <-ch2:
            fmt.Println("ch2:", msg)
        case <-time.After(3 * time.Second):
            fmt.Println("Timeout!")
        }
    }
}

WaitGroup

func main() {
    var wg sync.WaitGroup

    urls := []string{
        "https://example.com",
        "https://golang.org",
        "https://github.com",
    }

    for _, url := range urls {
        wg.Add(1)
        go func(u string) {
            defer wg.Done()
            resp, err := http.Get(u)
            if err != nil {
                fmt.Printf("Error: %s - %v\n", u, err)
                return
            }
            defer resp.Body.Close()
            fmt.Printf("%s -> %s\n", u, resp.Status)
        }(url)
    }

    wg.Wait() // wait for all goroutines
    fmt.Println("All requests complete")
}

Deadlock Prevention Patterns

// Bad: deadlock
func bad() {
    ch := make(chan int) // unbuffered channel
    ch <- 1             // blocks forever - no receiver
    fmt.Println(<-ch)
}

// Good 1: send in a goroutine
func good1() {
    ch := make(chan int)
    go func() { ch <- 1 }()
    fmt.Println(<-ch)
}

// Good 2: use buffered channel
func good2() {
    ch := make(chan int, 1)
    ch <- 1
    fmt.Println(<-ch)
}

Worker Pool Pattern

func workerPool(jobs <-chan int, results chan<- int, id int) {
    for j := range jobs {
        fmt.Printf("Worker %d processing: job %d\n", id, j)
        time.Sleep(time.Millisecond * 500)
        results <- j * 2
    }
}

func main() {
    const numJobs = 10
    const numWorkers = 3

    jobs := make(chan int, numJobs)
    results := make(chan int, numJobs)

    // Start workers
    for w := 1; w <= numWorkers; w++ {
        go workerPool(jobs, results, w)
    }

    // Send jobs
    for j := 1; j <= numJobs; j++ {
        jobs <- j
    }
    close(jobs)

    // Collect results
    for r := 1; r <= numJobs; r++ {
        result := <-results
        fmt.Printf("Result: %d\n", result)
    }
}

4. Interfaces

Go interfaces are implemented implicitly. There is no implements keyword.

Basic Interface

type Shape interface {
    Area() float64
    Perimeter() float64
}

type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

func (c Circle) Perimeter() float64 {
    return 2 * math.Pi * c.Radius
}

type Rectangle struct {
    Width, Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

// Function that accepts the Shape interface
func printInfo(s Shape) {
    fmt.Printf("Area: %.2f, Perimeter: %.2f\n", s.Area(), s.Perimeter())
}

io.Reader / io.Writer

The most powerful interfaces in the Go standard library.

// io.Reader interface
// type Reader interface {
//     Read(p []byte) (n int, err error)
// }

// io.Writer interface
// type Writer interface {
//     Write(p []byte) (n int, err error)
// }

func countBytes(r io.Reader) (int, error) {
    buf := make([]byte, 1024)
    total := 0
    for {
        n, err := r.Read(buf)
        total += n
        if err == io.EOF {
            return total, nil
        }
        if err != nil {
            return total, err
        }
    }
}

func main() {
    // Read from string
    r := strings.NewReader("Hello, Go!")
    n, _ := countBytes(r)
    fmt.Printf("%d bytes\n", n) // 10 bytes

    // Read from file
    f, _ := os.Open("data.txt")
    defer f.Close()
    n, _ = countBytes(f)
    fmt.Printf("%d bytes\n", n)
}

Empty Interface and Type Assertions

func describe(i interface{}) {
    // Type assertion
    if s, ok := i.(string); ok {
        fmt.Println("String:", s)
        return
    }

    // Type switch
    switch v := i.(type) {
    case int:
        fmt.Println("Integer:", v)
    case float64:
        fmt.Println("Float:", v)
    case bool:
        fmt.Println("Boolean:", v)
    default:
        fmt.Printf("Unknown type: %T\n", v)
    }
}

func main() {
    describe(42)
    describe("hello")
    describe(3.14)
    describe(true)
}

Interface Composition

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

type Closer interface {
    Close() error
}

// Interface composition (embedding)
type ReadWriter interface {
    Reader
    Writer
}

type ReadWriteCloser interface {
    Reader
    Writer
    Closer
}

5. Error Handling

Go uses explicit error returns instead of exceptions.

errors.New and fmt.Errorf

import (
    "errors"
    "fmt"
)

var ErrNotFound = errors.New("not found")

func findUser(id int) (*User, error) {
    if id <= 0 {
        return nil, fmt.Errorf("invalid user ID: %d", id)
    }
    // ... DB query
    return nil, ErrNotFound
}

Error Wrapping with errors.Is / errors.As

type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation failed - %s: %s", e.Field, e.Message)
}

func validateAge(age int) error {
    if age < 0 || age > 150 {
        return &ValidationError{
            Field:   "age",
            Message: fmt.Sprintf("age must be 0-150 (got: %d)", age),
        }
    }
    return nil
}

func processUser(age int) error {
    if err := validateAge(age); err != nil {
        return fmt.Errorf("user processing failed: %w", err) // wrap error
    }
    return nil
}

func main() {
    err := processUser(-5)

    // errors.Is: check for a specific error in the chain
    if errors.Is(err, ErrNotFound) {
        fmt.Println("User not found")
    }

    // errors.As: convert to a specific type in the chain
    var valErr *ValidationError
    if errors.As(err, &valErr) {
        fmt.Printf("Field: %s, Message: %s\n", valErr.Field, valErr.Message)
    }
}

Custom Error Pattern

type AppError struct {
    Code    int
    Message string
    Err     error
}

func (e *AppError) Error() string {
    if e.Err != nil {
        return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Err)
    }
    return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}

func (e *AppError) Unwrap() error {
    return e.Err
}

// Error creation helper
func NewAppError(code int, msg string, err error) *AppError {
    return &AppError{Code: code, Message: msg, Err: err}
}

6. Generics (Go 1.18+)

Generics were introduced in Go 1.18, enabling type-safe generic code.

Type Parameters

func Min[T constraints.Ordered](a, b T) T {
    if a < b {
        return a
    }
    return b
}

func main() {
    fmt.Println(Min(3, 5))       // int
    fmt.Println(Min(3.14, 2.71)) // float64
    fmt.Println(Min("a", "b"))   // string
}

Constraints

import "golang.org/x/exp/constraints"

// Custom constraint
type Number interface {
    constraints.Integer | constraints.Float
}

func Sum[T Number](nums []T) T {
    var total T
    for _, n := range nums {
        total += n
    }
    return total
}

// Generics with structs
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
}

func (s *Stack[T]) Len() int {
    return len(s.items)
}

Practical Generic Example: Map Utilities

func Keys[K comparable, V any](m map[K]V) []K {
    keys := make([]K, 0, len(m))
    for k := range m {
        keys = append(keys, k)
    }
    return keys
}

func Values[K comparable, V any](m map[K]V) []V {
    vals := make([]V, 0, len(m))
    for _, v := range m {
        vals = append(vals, v)
    }
    return vals
}

func Filter[T any](slice []T, predicate func(T) bool) []T {
    var result []T
    for _, item := range slice {
        if predicate(item) {
            result = append(result, item)
        }
    }
    return result
}

func Map[T any, U any](slice []T, transform func(T) U) []U {
    result := make([]U, len(slice))
    for i, item := range slice {
        result[i] = transform(item)
    }
    return result
}

7. Testing

Go has a powerful built-in testing framework. No external libraries are needed.

Basic Tests

// math.go
package math

func Add(a, b int) int {
    return a + b
}

func Fibonacci(n int) int {
    if n <= 1 {
        return n
    }
    return Fibonacci(n-1) + Fibonacci(n-2)
}
// math_test.go
package math

import "testing"

func TestAdd(t *testing.T) {
    result := Add(2, 3)
    if result != 5 {
        t.Errorf("Add(2, 3) = %d; want 5", result)
    }
}

Table-Driven Tests

func TestFibonacci(t *testing.T) {
    tests := []struct {
        name     string
        input    int
        expected int
    }{
        {"zero", 0, 0},
        {"one", 1, 1},
        {"two", 2, 1},
        {"five", 5, 5},
        {"ten", 10, 55},
    }

    for _, tc := range tests {
        t.Run(tc.name, func(t *testing.T) {
            result := Fibonacci(tc.input)
            if result != tc.expected {
                t.Errorf("Fibonacci(%d) = %d; want %d",
                    tc.input, result, tc.expected)
            }
        })
    }
}

Benchmarks

func BenchmarkFibonacci(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Fibonacci(20)
    }
}

// Run: go test -bench=. -benchmem
// BenchmarkFibonacci-8  28735  41523 ns/op  0 B/op  0 allocs/op

httptest

func TestHealthHandler(t *testing.T) {
    req := httptest.NewRequest("GET", "/health", nil)
    w := httptest.NewRecorder()

    healthHandler(w, req)

    resp := w.Result()
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        t.Errorf("status code = %d; want %d",
            resp.StatusCode, http.StatusOK)
    }

    body, _ := io.ReadAll(resp.Body)
    if string(body) != `{"status":"ok"}` {
        t.Errorf("response = %s; want {\"status\":\"ok\"}", body)
    }
}

8. Packages and Modules

Initializing go mod

# Start a new project
mkdir myproject && cd myproject
go mod init github.com/username/myproject

# Add dependencies
go get github.com/gin-gonic/gin@latest

# Clean up unused dependencies
go mod tidy

# Download dependencies
go mod download

Project Structure

myproject/
  cmd/
    server/
      main.go          # Entry point
  internal/
    handler/
      user.go          # HTTP handlers
      user_test.go
    service/
      user.go          # Business logic
    repository/
      user.go          # Data access
  pkg/
    validator/
      validator.go     # Publicly shared utilities
  go.mod
  go.sum

The internal Package

Packages inside the internal directory cannot be imported from outside the module. This clearly separates the public API from internal implementation.

// internal/config/config.go
package config

type Config struct {
    Port     int
    DBHost   string
    LogLevel string
}

func Load() (*Config, error) {
    // Load config from environment variables
    return &Config{
        Port:     8080,
        DBHost:   "localhost:5432",
        LogLevel: "info",
    }, nil
}

9. Practical Patterns

Web Server (net/http)

package main

import (
    "encoding/json"
    "log"
    "net/http"
    "time"
)

type Response struct {
    Message   string `json:"message"`
    Timestamp string `json:"timestamp"`
}

func healthHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(Response{
        Message:   "ok",
        Timestamp: time.Now().Format(time.RFC3339),
    })
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /health", healthHandler)
    mux.HandleFunc("GET /api/users", getUsersHandler)

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

    log.Printf("Server starting: %s", server.Addr)
    log.Fatal(server.ListenAndServe())
}

Middleware

func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        log.Printf("[%s] %s started", r.Method, r.URL.Path)

        next.ServeHTTP(w, r)

        log.Printf("[%s] %s completed (%v)",
            r.Method, r.URL.Path, time.Since(start))
    })
}

func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        if token == "" {
            http.Error(w, "Authentication required", http.StatusUnauthorized)
            return
        }
        // Token validation logic...
        next.ServeHTTP(w, r)
    })
}

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

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /health", healthHandler)

    handler := chain(mux, loggingMiddleware, authMiddleware)

    log.Fatal(http.ListenAndServe(":8080", handler))
}

Graceful Shutdown

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /health", healthHandler)

    server := &http.Server{
        Addr:    ":8080",
        Handler: mux,
    }

    // Start server in a goroutine
    go func() {
        log.Println("Server starting: :8080")
        if err := server.ListenAndServe(); err != http.ErrServerClosed {
            log.Fatalf("Server error: %v", err)
        }
    }()

    // Wait for shutdown signal
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit
    log.Println("Server shutting down...")

    // Graceful shutdown with 30s timeout
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    if err := server.Shutdown(ctx); err != nil {
        log.Fatalf("Server forced to shutdown: %v", err)
    }

    log.Println("Server shutdown complete")
}

Configuration Management

type Config struct {
    Server   ServerConfig
    Database DatabaseConfig
}

type ServerConfig struct {
    Port         int           `json:"port"`
    ReadTimeout  time.Duration `json:"read_timeout"`
    WriteTimeout time.Duration `json:"write_timeout"`
}

type DatabaseConfig struct {
    Host     string `json:"host"`
    Port     int    `json:"port"`
    User     string `json:"user"`
    Password string `json:"password"`
    DBName   string `json:"dbname"`
}

func (d DatabaseConfig) DSN() string {
    return fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
        d.Host, d.Port, d.User, d.Password, d.DBName)
}

func LoadConfig(path string) (*Config, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, fmt.Errorf("failed to read config file: %w", err)
    }

    var cfg Config
    if err := json.Unmarshal(data, &cfg); err != nil {
        return nil, fmt.Errorf("failed to parse config: %w", err)
    }

    return &cfg, nil
}

10. Go in 2026

Cloud Infrastructure

Go is the de facto standard language of the cloud-native ecosystem.

  • Container Orchestration: Kubernetes, Docker, containerd
  • Service Mesh: Istio, Linkerd
  • Monitoring: Prometheus, Grafana Agent, Thanos
  • IaC: Terraform, Pulumi
  • CI/CD: Drone, Tekton

CLI Tools

CLI tools built with Go ship as a single binary, making installation effortless.

// CLI example using cobra
package main

import (
    "fmt"
    "os"

    "github.com/spf13/cobra"
)

var rootCmd = &cobra.Command{
    Use:   "mytool",
    Short: "My CLI tool",
}

var greetCmd = &cobra.Command{
    Use:   "greet [name]",
    Short: "Say hello",
    Args:  cobra.ExactArgs(1),
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Printf("Hello, %s!\n", args[0])
    },
}

func main() {
    rootCmd.AddCommand(greetCmd)
    if err := rootCmd.Execute(); err != nil {
        os.Exit(1)
    }
}

Microservices

Go is optimized for microservice architectures.

  • Fast startup time: Minimizes cold start latency
  • Low memory usage: Saves container resources
  • Static binary: Lightweight Docker images (scratch base possible)
  • Native gRPC support: Service-to-service communication with protobuf + gRPC
  • OpenTelemetry: Distributed tracing and metrics collection
# Multi-stage build
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /server ./cmd/server

FROM scratch
COPY --from=builder /server /server
EXPOSE 8080
ENTRYPOINT ["/server"]

The final image size is around 10-20MB — less than 1/10 of Java or Python-based services.


Conclusion

Go is a language where simplicity is power. It excludes complex features and provides only what is essential. This philosophy is why Go is chosen across a broad range of domains, from infrastructure tools like Docker and Kubernetes to CLI tools and microservices.

The best way to start is to write code yourself. Try building a simple CLI tool or a REST API server.

Quiz: Test Your Go Concurrency Knowledge

Q1. What is the difference between a goroutine and an OS thread?

The Go runtime multiplexes goroutines onto a small number of OS threads. A goroutine starts with a stack of about 2KB, much lighter than the 1-8MB stack of an OS thread.

Q2. What is the relationship between sending and receiving on an unbuffered channel?

The sender blocks until a receiver is ready, and the receiver blocks until a sender is ready. It is synchronous communication.

Q3. What is the role of the select statement?

It waits on multiple channel operations simultaneously, processing whichever channel is ready first. It is useful for timeout and cancellation handling.