Skip to content

Split View: Go 언어 완전 문법 가이드: 기초부터 동시성, 제네릭까지

|

Go 언어 완전 문법 가이드: 기초부터 동시성, 제네릭까지

소개

Go(Golang)는 Google이 2009년 발표한 정적 타입, 컴파일형 언어입니다. 단순한 문법, 빠른 컴파일, 강력한 동시성 지원, 풍부한 표준 라이브러리가 특징입니다. 클라우드 인프라, 마이크로서비스, CLI 도구 개발에 특히 강력합니다.

이 가이드는 Go의 핵심 문법을 처음부터 끝까지 체계적으로 정리합니다.


1. Go 언어 기초 문법

1.1 변수 선언

Go에서 변수를 선언하는 방법은 여러 가지입니다.

package main

import "fmt"

func main() {
    // var 키워드로 선언 (타입 명시)
    var name string = "Go"
    var age int = 15

    // 타입 추론 (var)
    var pi = 3.14159

    // 단축 선언 (:=) - 함수 내부에서만 사용 가능
    greeting := "Hello, Go!"
    count := 42

    // 복수 변수 선언
    var x, y int = 10, 20
    a, b := 100, 200

    // 제로값: 선언 시 초기화하지 않으면 타입의 기본값
    var zeroInt int       // 0
    var zeroStr string    // ""
    var zeroBool bool     // false

    fmt.Println(name, age, pi, greeting, count)
    fmt.Println(x, y, a, b)
    fmt.Println(zeroInt, zeroStr, zeroBool)
}

1.2 상수와 iota

package main

import "fmt"

// 단순 상수
const Pi = 3.14159
const MaxSize = 1024

// 상수 블록
const (
    StatusOK    = 200
    StatusNotFound = 404
    StatusError = 500
)

// iota: 열거형 상수 자동 증가
type Direction int

const (
    North Direction = iota // 0
    East                   // 1
    South                  // 2
    West                   // 3
)

type ByteSize float64

const (
    _           = iota // blank identifier로 0 건너뜀
    KB ByteSize = 1 << (10 * iota) // 1 << 10 = 1024
    MB                              // 1 << 20
    GB                              // 1 << 30
)

func main() {
    fmt.Println(North, East, South, West) // 0 1 2 3
    fmt.Printf("KB=%.0f, MB=%.0f, GB=%.0f\n", float64(KB), float64(MB), float64(GB))
}

1.3 기본 타입

package main

import "fmt"

func main() {
    // 정수 타입
    var i8 int8 = 127
    var i16 int16 = 32767
    var i32 int32 = 2147483647
    var i64 int64 = 9223372036854775807
    var ui uint = 42
    var ui64 uint64 = 18446744073709551615

    // 플랫폼 의존 (64비트 시스템에서 int64)
    var n int = 100

    // 부동소수점
    var f32 float32 = 3.14
    var f64 float64 = 3.141592653589793

    // 복소수
    var c128 complex128 = 3 + 4i

    // 문자열 (UTF-8 인코딩, 불변)
    s := "Hello, 세계"
    fmt.Println(len(s))        // 바이트 수
    fmt.Println([]rune(s))     // 룬(유니코드 코드포인트) 슬라이스

    // byte (uint8의 별칭)
    var b byte = 'A'

    // rune (int32의 별칭, 유니코드 코드포인트)
    var r rune = '한'

    // bool
    var flag bool = true

    fmt.Println(i8, i16, i32, i64, ui, ui64, n)
    fmt.Println(f32, f64, c128)
    fmt.Println(b, r, flag)
}

1.4 배열과 슬라이스

package main

import "fmt"

func main() {
    // 배열: 고정 크기, 값 타입
    var arr [5]int
    arr[0] = 10
    arr2 := [3]string{"Go", "Python", "Rust"}
    arr3 := [...]int{1, 2, 3, 4, 5} // 컴파일러가 크기 추론

    fmt.Println(arr, arr2, arr3)

    // 슬라이스: 동적 크기, 참조 타입
    s1 := []int{1, 2, 3}
    s2 := make([]int, 3)       // len=3, cap=3
    s3 := make([]int, 3, 10)   // len=3, cap=10

    fmt.Println(len(s1), cap(s1)) // 3, 3
    fmt.Println(len(s2), cap(s2)) // 3, 3
    fmt.Println(len(s3), cap(s3)) // 3, 10

    // append: 용량 초과 시 새 배열 할당
    s1 = append(s1, 4, 5)
    fmt.Println(s1) // [1 2 3 4 5]

    // 슬라이싱
    s4 := s1[1:3]  // [2 3] (원본과 메모리 공유)
    s5 := s1[2:]   // [3 4 5]
    s6 := s1[:2]   // [1 2]

    fmt.Println(s4, s5, s6)

    // copy: 독립적인 복사본 생성
    dst := make([]int, len(s1))
    n := copy(dst, s1)
    fmt.Printf("복사된 원소 수: %d, dst: %v\n", n, dst)

    // 2차원 슬라이스
    matrix := make([][]int, 3)
    for i := range matrix {
        matrix[i] = make([]int, 3)
        for j := range matrix[i] {
            matrix[i][j] = i*3 + j
        }
    }
    fmt.Println(matrix)
}

1.5 맵

package main

import "fmt"

func main() {
    // 맵 선언과 초기화
    m1 := map[string]int{
        "apple":  5,
        "banana": 3,
        "cherry": 8,
    }

    m2 := make(map[string][]string)

    // 값 추가/수정
    m1["date"] = 12
    m2["fruits"] = []string{"apple", "banana"}

    // 값 읽기
    val := m1["apple"]
    fmt.Println(val) // 5

    // comma-ok 패턴: 키 존재 여부 확인
    val2, ok := m1["mango"]
    if !ok {
        fmt.Println("mango not found, zero value:", val2)
    }

    // 삭제
    delete(m1, "cherry")
    fmt.Println(m1)

    // 순회 (순서 보장 안 됨)
    for key, value := range m1 {
        fmt.Printf("%s: %d\n", key, value)
    }

    // nil 맵에 쓰기는 패닉 발생
    var m3 map[string]int
    // m3["key"] = 1 // panic: assignment to entry in nil map
    _ = m3
}

1.6 포인터

package main

import "fmt"

func increment(n *int) {
    *n++ // 역참조
}

func newInt(val int) *int {
    return &val // 로컬 변수의 주소 반환 가능 (힙 이동)
}

func main() {
    x := 42
    p := &x        // x의 주소
    fmt.Println(p)  // 메모리 주소 출력 (예: 0xc0000b4010)
    fmt.Println(*p) // 42 (역참조)

    *p = 100
    fmt.Println(x) // 100

    increment(&x)
    fmt.Println(x) // 101

    ptr := newInt(200)
    fmt.Println(*ptr) // 200

    // nil 포인터
    var nilPtr *int
    fmt.Println(nilPtr) // <nil>
    // fmt.Println(*nilPtr) // panic: nil pointer dereference
}

1.7 제어문

package main

import "fmt"

func main() {
    // for: Go의 유일한 반복문
    for i := 0; i < 5; i++ {
        fmt.Print(i, " ")
    }
    fmt.Println()

    // while 스타일
    n := 1
    for n < 100 {
        n *= 2
    }
    fmt.Println(n)

    // range
    nums := []int{10, 20, 30, 40}
    for idx, val := range nums {
        fmt.Printf("[%d]=%d ", idx, val)
    }
    fmt.Println()

    // 인덱스만 사용
    for i := range nums {
        fmt.Print(nums[i], " ")
    }
    fmt.Println()

    // switch (break 없이도 fallthrough 안 됨)
    day := "Monday"
    switch day {
    case "Saturday", "Sunday":
        fmt.Println("주말")
    case "Monday":
        fmt.Println("월요일")
    default:
        fmt.Println("평일")
    }

    // 조건 없는 switch (if-else 대체)
    score := 85
    switch {
    case score >= 90:
        fmt.Println("A")
    case score >= 80:
        fmt.Println("B")
    case score >= 70:
        fmt.Println("C")
    default:
        fmt.Println("F")
    }

    // defer: 함수 종료 시 실행 (LIFO 순서)
    fmt.Println("시작")
    defer fmt.Println("첫 번째 defer")
    defer fmt.Println("두 번째 defer")
    fmt.Println("끝")
    // 출력: 시작 -> 끝 -> 두 번째 defer -> 첫 번째 defer
}

2. 함수와 메서드

2.1 다중 반환값과 Named Return

package main

import (
    "errors"
    "fmt"
)

// 다중 반환값
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

// Named return values
func minMax(nums []int) (min, max int) {
    if len(nums) == 0 {
        return // 제로값 반환
    }
    min, max = nums[0], nums[0]
    for _, n := range nums[1:] {
        if n < min {
            min = n
        }
        if n > max {
            max = n
        }
    }
    return // naked return
}

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

func main() {
    result, err := divide(10, 3)
    if err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Printf("%.4f\n", result) // 3.3333
    }

    _, err = divide(5, 0)
    if err != nil {
        fmt.Println("Error:", err) // division by zero
    }

    min, max := minMax([]int{3, 1, 4, 1, 5, 9, 2, 6})
    fmt.Println(min, max) // 1 9

    fmt.Println(sum(1, 2, 3, 4, 5)) // 15

    // 슬라이스를 variadic 인수로 펼치기
    nums := []int{10, 20, 30}
    fmt.Println(sum(nums...)) // 60
}

2.2 일급 함수와 클로저

package main

import "fmt"

// 함수를 인수로 받기
func apply(nums []int, fn func(int) int) []int {
    result := make([]int, len(nums))
    for i, n := range nums {
        result[i] = fn(n)
    }
    return result
}

// 함수를 반환하기
func multiplier(factor int) func(int) int {
    return func(n int) int {
        return n * factor
    }
}

// 클로저: 외부 변수를 캡처
func counter() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}

func main() {
    double := func(n int) int { return n * 2 }
    nums := []int{1, 2, 3, 4, 5}

    fmt.Println(apply(nums, double))    // [2 4 6 8 10]
    fmt.Println(apply(nums, func(n int) int { return n * n })) // [1 4 9 16 25]

    triple := multiplier(3)
    fmt.Println(triple(7)) // 21

    c1 := counter()
    c2 := counter()
    fmt.Println(c1(), c1(), c1()) // 1 2 3
    fmt.Println(c2(), c2())       // 1 2 (독립적인 상태)
}

2.3 메서드 (Value Receiver vs Pointer Receiver)

package main

import (
    "fmt"
    "math"
)

type Circle struct {
    Radius float64
}

// Value receiver: 복사본에 적용 (원본 변경 불가)
func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

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

// Pointer receiver: 원본 변경 가능
func (c *Circle) Scale(factor float64) {
    c.Radius *= factor
}

type Rectangle struct {
    Width, Height float64
}

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

// init() 함수: 패키지 초기화 시 자동 실행
func init() {
    fmt.Println("패키지 초기화")
}

func main() {
    c := Circle{Radius: 5}
    fmt.Printf("넓이: %.2f\n", c.Area())
    fmt.Printf("둘레: %.2f\n", c.Perimeter())

    c.Scale(2)
    fmt.Printf("스케일 후 반지름: %.1f\n", c.Radius) // 10.0

    // 포인터 메서드를 값에서 호출 가능 (자동 주소 취득)
    c2 := Circle{Radius: 3}
    (&c2).Scale(1.5) // 자동으로 &c2로 변환
}

3. 구조체와 인터페이스

3.1 구조체와 임베딩

package main

import "fmt"

type Animal struct {
    Name string
    Age  int
}

func (a Animal) Speak() string {
    return a.Name + " makes a sound"
}

// 임베딩: 상속이 아닌 구성(Composition)
type Dog struct {
    Animal              // 임베딩 (익명 필드)
    Breed  string
}

func (d Dog) Speak() string {
    return d.Name + " says: Woof!"
}

type ServiceDog struct {
    Dog
    ServiceType string
}

func main() {
    d := Dog{
        Animal: Animal{Name: "Buddy", Age: 3},
        Breed:  "Labrador",
    }

    // 임베딩된 필드 직접 접근
    fmt.Println(d.Name)  // d.Animal.Name과 동일
    fmt.Println(d.Age)
    fmt.Println(d.Breed)
    fmt.Println(d.Speak()) // Dog의 Speak() 호출
    fmt.Println(d.Animal.Speak()) // Animal의 Speak() 명시적 호출

    sd := ServiceDog{
        Dog:         d,
        ServiceType: "Guide",
    }
    fmt.Println(sd.Name)        // sd.Dog.Animal.Name
    fmt.Println(sd.ServiceType)
}

3.2 인터페이스

package main

import (
    "fmt"
    "math"
)

// 인터페이스 정의
type Shape interface {
    Area() float64
    Perimeter() float64
}

type Stringer interface {
    String() string
}

type Circle struct{ Radius float64 }
type Rectangle struct{ Width, Height float64 }

func (c Circle) Area() float64      { return math.Pi * c.Radius * c.Radius }
func (c Circle) Perimeter() float64 { return 2 * math.Pi * c.Radius }
func (c Circle) String() string     { return fmt.Sprintf("Circle(r=%.1f)", c.Radius) }

func (r Rectangle) Area() float64      { return r.Width * r.Height }
func (r Rectangle) Perimeter() float64 { return 2 * (r.Width + r.Height) }
func (r Rectangle) String() string {
    return fmt.Sprintf("Rect(%.1fx%.1f)", r.Width, r.Height)
}

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

// 빈 인터페이스 (모든 타입)
func printAny(v interface{}) {
    fmt.Printf("타입: %T, 값: %v\n", v, v)
}

// 타입 어서션
func describe(i interface{}) {
    if s, ok := i.(string); ok {
        fmt.Printf("문자열: %q\n", s)
        return
    }
    if n, ok := i.(int); ok {
        fmt.Printf("정수: %d\n", n)
        return
    }
    fmt.Printf("알 수 없는 타입: %T\n", i)
}

// 타입 스위치
func typeSwitch(v interface{}) string {
    switch x := v.(type) {
    case int:
        return fmt.Sprintf("int: %d", x)
    case string:
        return fmt.Sprintf("string: %q", x)
    case bool:
        return fmt.Sprintf("bool: %v", x)
    case []int:
        return fmt.Sprintf("[]int with %d elements", len(x))
    default:
        return fmt.Sprintf("unknown type: %T", x)
    }
}

func main() {
    shapes := []Shape{
        Circle{Radius: 5},
        Rectangle{Width: 4, Height: 6},
    }

    for _, s := range shapes {
        printShapeInfo(s)
        if str, ok := s.(Stringer); ok {
            fmt.Println(str.String())
        }
    }

    printAny(42)
    printAny("hello")
    printAny([]int{1, 2, 3})

    describe("Go언어")
    describe(2024)

    fmt.Println(typeSwitch(100))
    fmt.Println(typeSwitch("world"))
    fmt.Println(typeSwitch([]int{1, 2}))
}

4. 에러 처리

4.1 에러 타입과 커스텀 에러

package main

import (
    "errors"
    "fmt"
)

// 센티널 에러 (패키지 수준 에러 변수)
var (
    ErrNotFound   = errors.New("not found")
    ErrPermission = errors.New("permission denied")
)

// 커스텀 에러 타입
type ValidationError struct {
    Field   string
    Message string
}

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

type DatabaseError struct {
    Code    int
    Message string
    Err     error // wrapped error
}

func (e *DatabaseError) Error() string {
    return fmt.Sprintf("database error [%d]: %s", e.Code, e.Message)
}

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

// fmt.Errorf로 에러 래핑
func findUser(id int) error {
    if id <= 0 {
        return fmt.Errorf("findUser: invalid id %d: %w", id, ErrNotFound)
    }
    return nil
}

func getUser(id int) error {
    if err := findUser(id); err != nil {
        return fmt.Errorf("getUser: %w", err)
    }
    return nil
}

func main() {
    // errors.Is: 에러 체인에서 특정 에러 확인
    err := getUser(-1)
    if errors.Is(err, ErrNotFound) {
        fmt.Println("not found 에러:", err)
    }

    // errors.As: 에러 체인에서 특정 타입 추출
    dbErr := &DatabaseError{
        Code:    500,
        Message: "connection failed",
        Err:     ErrPermission,
    }
    wrapped := fmt.Errorf("service error: %w", dbErr)

    var de *DatabaseError
    if errors.As(wrapped, &de) {
        fmt.Printf("DB 에러 코드: %d\n", de.Code)
    }

    if errors.Is(wrapped, ErrPermission) {
        fmt.Println("권한 에러가 체인에 포함됨")
    }

    // 커스텀 에러 생성
    valErr := &ValidationError{Field: "email", Message: "invalid format"}
    fmt.Println(valErr)
}

4.2 panic과 recover

package main

import "fmt"

// recover로 panic 복구
func safeDiv(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("recovered from panic: %v", r)
        }
    }()
    return a / b, nil
}

func mustPositive(n int) int {
    if n <= 0 {
        panic(fmt.Sprintf("값은 양수여야 합니다: %d", n))
    }
    return n
}

func main() {
    result, err := safeDiv(10, 2)
    fmt.Printf("10/2 = %d, err = %v\n", result, err)

    result, err = safeDiv(10, 0)
    fmt.Printf("10/0 = %d, err = %v\n", result, err)

    // 정상적인 panic 사용: 프로그래밍 오류나 복구 불가능한 상황
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("패닉 복구:", r)
        }
    }()
    mustPositive(-5)
}

5. 고루틴과 채널 (동시성)

5.1 고루틴과 WaitGroup

package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Printf("Worker %d 시작\n", id)
    time.Sleep(time.Millisecond * 100)
    fmt.Printf("Worker %d 완료\n", id)
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 5; i++ {
        wg.Add(1)
        go worker(i, &wg)
    }

    wg.Wait()
    fmt.Println("모든 워커 완료")
}

5.2 채널

package main

import (
    "fmt"
    "time"
)

func producer(ch chan<- int, count int) {
    for i := 0; i < count; i++ {
        ch <- i
        time.Sleep(time.Millisecond * 10)
    }
    close(ch)
}

func main() {
    // 언버퍼드 채널 (동기)
    ch1 := make(chan int)
    go func() { ch1 <- 42 }()
    fmt.Println(<-ch1) // 42

    // 버퍼드 채널 (비동기, 버퍼 차기 전까지 블록 안 됨)
    ch2 := make(chan string, 3)
    ch2 <- "first"
    ch2 <- "second"
    ch2 <- "third"
    fmt.Println(<-ch2) // first

    // 채널 순회
    numbers := make(chan int, 10)
    go producer(numbers, 5)

    for n := range numbers {
        fmt.Print(n, " ")
    }
    fmt.Println()

    // select 문: 여러 채널 동시 처리
    ch3 := make(chan string, 1)
    ch4 := make(chan string, 1)

    go func() {
        time.Sleep(time.Millisecond * 50)
        ch3 <- "채널3"
    }()
    go func() {
        time.Sleep(time.Millisecond * 30)
        ch4 <- "채널4"
    }()

    for i := 0; i < 2; i++ {
        select {
        case msg := <-ch3:
            fmt.Println("수신:", msg)
        case msg := <-ch4:
            fmt.Println("수신:", msg)
        }
    }
}

5.3 Mutex와 동기화 프리미티브

package main

import (
    "fmt"
    "sync"
)

type SafeCounter struct {
    mu sync.Mutex
    v  map[string]int
}

func (c *SafeCounter) Inc(key string) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.v[key]++
}

func (c *SafeCounter) Value(key string) int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.v[key]
}

// RWMutex: 읽기는 동시, 쓰기는 독점
type Cache struct {
    mu   sync.RWMutex
    data map[string]string
}

func (c *Cache) Set(key, value string) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.data[key] = value
}

func (c *Cache) Get(key string) (string, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    val, ok := c.data[key]
    return val, ok
}

// sync.Once: 한 번만 실행
var (
    instance *Cache
    once     sync.Once
)

func getInstance() *Cache {
    once.Do(func() {
        instance = &Cache{data: make(map[string]string)}
        fmt.Println("캐시 인스턴스 생성")
    })
    return instance
}

func main() {
    counter := SafeCounter{v: make(map[string]int)}
    var wg sync.WaitGroup

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            counter.Inc("key")
        }()
    }
    wg.Wait()
    fmt.Println("카운터:", counter.Value("key")) // 1000

    // sync.Map: 동시성 안전한 맵
    var sm sync.Map
    sm.Store("go", "1.21")
    sm.Store("python", "3.12")

    sm.Range(func(key, value interface{}) bool {
        fmt.Printf("%v: %v\n", key, value)
        return true
    })

    c1 := getInstance()
    c2 := getInstance()
    fmt.Println(c1 == c2) // true (같은 인스턴스)
}

5.4 context.Context

package main

import (
    "context"
    "fmt"
    "time"
)

func doWork(ctx context.Context, id int) error {
    select {
    case <-time.After(time.Millisecond * 200):
        fmt.Printf("작업 %d 완료\n", id)
        return nil
    case <-ctx.Done():
        fmt.Printf("작업 %d 취소: %v\n", id, ctx.Err())
        return ctx.Err()
    }
}

func fetchData(ctx context.Context, url string) (string, error) {
    // 컨텍스트에서 값 읽기
    if requestID, ok := ctx.Value("requestID").(string); ok {
        fmt.Printf("요청 ID: %s, URL: %s\n", requestID, url)
    }
    select {
    case <-time.After(time.Millisecond * 100):
        return "data from " + url, nil
    case <-ctx.Done():
        return "", ctx.Err()
    }
}

func main() {
    // WithCancel
    ctx, cancel := context.WithCancel(context.Background())
    go func() {
        time.Sleep(time.Millisecond * 50)
        cancel() // 취소 신호 전송
    }()
    doWork(ctx, 1)

    // WithTimeout
    ctx2, cancel2 := context.WithTimeout(context.Background(), time.Millisecond*150)
    defer cancel2()
    doWork(ctx2, 2)

    // WithDeadline
    deadline := time.Now().Add(time.Millisecond * 300)
    ctx3, cancel3 := context.WithDeadline(context.Background(), deadline)
    defer cancel3()
    doWork(ctx3, 3)

    // WithValue: 요청 범위 데이터 전달
    ctx4 := context.WithValue(context.Background(), "requestID", "req-001")
    data, err := fetchData(ctx4, "https://api.example.com/users")
    if err == nil {
        fmt.Println("데이터:", data)
    }
}

6. 패키지와 모듈

6.1 Go Modules

# 새 모듈 초기화
go mod init github.com/username/myapp

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

# 의존성 정리
go mod tidy

# 벤더 디렉토리 생성
go mod vendor

go.mod 파일 예시:

module github.com/username/myapp

go 1.21

require (
    github.com/gin-gonic/gin v1.9.1
    github.com/stretchr/testify v1.8.4
)

6.2 패키지 구조 베스트 프랙티스

myapp/
├── cmd/
│   └── server/
│       └── main.go          # 진입점
├── internal/                 # 외부 패키지에서 임포트 불가
│   ├── handler/
│   │   └── user.go
│   ├── service/
│   │   └── user.go
│   └── repository/
│       └── user.go
├── pkg/                     # 외부에서 임포트 가능한 라이브러리
│   └── validator/
│       └── validator.go
├── api/                     # API 정의 (OpenAPI, Protobuf)
├── config/
│   └── config.go
├── go.mod
└── go.sum

6.3 주요 표준 라이브러리

package main

import (
    "fmt"
    "os"
    "strings"
    "strconv"
    "time"
    "io"
    "bytes"
)

func main() {
    // strings 패키지
    s := "  Hello, Go World!  "
    fmt.Println(strings.TrimSpace(s))
    fmt.Println(strings.ToUpper(s))
    fmt.Println(strings.Contains(s, "Go"))
    fmt.Println(strings.Replace(s, "Go", "Golang", 1))
    parts := strings.Split("a,b,c,d", ",")
    fmt.Println(parts)
    fmt.Println(strings.Join(parts, " | "))

    // strconv 패키지
    n, _ := strconv.Atoi("42")
    fmt.Println(n + 1)
    str := strconv.Itoa(100)
    fmt.Println(str + " bottles")
    f, _ := strconv.ParseFloat("3.14", 64)
    fmt.Printf("%.2f\n", f)

    // time 패키지
    now := time.Now()
    fmt.Println(now.Format("2006-01-02 15:04:05"))
    fmt.Println(now.Format(time.RFC3339))
    tomorrow := now.Add(24 * time.Hour)
    fmt.Println(tomorrow.Sub(now))

    t, _ := time.Parse("2006-01-02", "2026-03-17")
    fmt.Println(t.Year(), t.Month(), t.Day())

    // os 패키지
    fmt.Println(os.Getenv("HOME"))
    args := os.Args // 커맨드라인 인수

    // io와 bytes
    buf := bytes.NewBufferString("Hello")
    buf.WriteString(", World")
    data, _ := io.ReadAll(buf)
    fmt.Println(string(data))

    _ = args
}

7. 제네릭 (Go 1.18+)

7.1 제네릭 함수와 타입

package main

import (
    "fmt"
    "golang.org/x/exp/constraints"
)

// 타입 파라미터를 사용한 제네릭 함수
func Min[T constraints.Ordered](a, b T) T {
    if a < b {
        return a
    }
    return b
}

func Map[T, 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, pred func(T) bool) []T {
    var result []T
    for _, v := range slice {
        if pred(v) {
            result = append(result, v)
        }
    }
    return result
}

func Reduce[T, U any](slice []T, initial U, fn func(U, T) U) U {
    acc := initial
    for _, v := range slice {
        acc = fn(acc, v)
    }
    return acc
}

// 제네릭 타입
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) {
    var zero T
    if len(s.items) == 0 {
        return zero, false
    }
    top := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return top, true
}

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

// comparable 제약: == 연산자 사용 가능
func Contains[T comparable](slice []T, item T) bool {
    for _, v := range slice {
        if v == item {
            return true
        }
    }
    return false
}

// 커스텀 타입 제약
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
}

func main() {
    fmt.Println(Min(3, 5))         // 3
    fmt.Println(Min(3.14, 2.71))   // 2.71
    fmt.Println(Min("apple", "banana")) // apple

    nums := []int{1, 2, 3, 4, 5}
    doubled := Map(nums, func(n int) int { return n * 2 })
    fmt.Println(doubled) // [2 4 6 8 10]

    strs := Map(nums, func(n int) string { return fmt.Sprintf("item_%d", n) })
    fmt.Println(strs)

    evens := Filter(nums, func(n int) bool { return n%2 == 0 })
    fmt.Println(evens) // [2 4]

    total := Reduce(nums, 0, func(acc, n int) int { return acc + n })
    fmt.Println(total) // 15

    var s Stack[string]
    s.Push("first")
    s.Push("second")
    s.Push("third")

    for s.Len() > 0 {
        item, _ := s.Pop()
        fmt.Println(item)
    }

    fmt.Println(Contains([]string{"go", "python", "rust"}, "go")) // true
    fmt.Println(Sum([]float64{1.1, 2.2, 3.3})) // 6.6
}

8. 코드 예시: REST API 서버 (net/http)

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "strconv"
    "strings"
    "sync"
)

type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

type UserStore struct {
    mu      sync.RWMutex
    users   map[int]User
    nextID  int
}

func NewUserStore() *UserStore {
    return &UserStore{
        users:  make(map[int]User),
        nextID: 1,
    }
}

func (s *UserStore) Create(name, email string) User {
    s.mu.Lock()
    defer s.mu.Unlock()
    user := User{ID: s.nextID, Name: name, Email: email}
    s.users[s.nextID] = user
    s.nextID++
    return user
}

func (s *UserStore) GetAll() []User {
    s.mu.RLock()
    defer s.mu.RUnlock()
    users := make([]User, 0, len(s.users))
    for _, u := range s.users {
        users = append(users, u)
    }
    return users
}

func (s *UserStore) GetByID(id int) (User, bool) {
    s.mu.RLock()
    defer s.mu.RUnlock()
    u, ok := s.users[id]
    return u, ok
}

// JSON 응답 헬퍼
func writeJSON(w http.ResponseWriter, status int, v interface{}) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(v)
}

// 핸들러 팩토리 (클로저로 의존성 주입)
func makeUsersHandler(store *UserStore) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        switch r.Method {
        case http.MethodGet:
            users := store.GetAll()
            writeJSON(w, http.StatusOK, users)

        case http.MethodPost:
            var body struct {
                Name  string `json:"name"`
                Email string `json:"email"`
            }
            if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
                writeJSON(w, http.StatusBadRequest, map[string]string{
                    "error": "invalid request body",
                })
                return
            }
            user := store.Create(body.Name, body.Email)
            writeJSON(w, http.StatusCreated, user)

        default:
            http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
        }
    }
}

func makeUserHandler(store *UserStore) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // URL에서 ID 추출: /api/users/123
        parts := strings.Split(r.URL.Path, "/")
        if len(parts) < 4 {
            http.Error(w, "Bad Request", http.StatusBadRequest)
            return
        }
        id, err := strconv.Atoi(parts[3])
        if err != nil {
            http.Error(w, "Invalid ID", http.StatusBadRequest)
            return
        }

        user, ok := store.GetByID(id)
        if !ok {
            writeJSON(w, http.StatusNotFound, map[string]string{
                "error": fmt.Sprintf("user %d not found", id),
            })
            return
        }
        writeJSON(w, http.StatusOK, user)
    }
}

// 미들웨어: 로깅
func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        log.Printf("%s %s", r.Method, r.URL.Path)
        next.ServeHTTP(w, r)
    })
}

func main() {
    store := NewUserStore()

    // 초기 데이터
    store.Create("Alice", "alice@example.com")
    store.Create("Bob", "bob@example.com")

    mux := http.NewServeMux()
    mux.Handle("/api/users", makeUsersHandler(store))
    mux.Handle("/api/users/", makeUserHandler(store))
    mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
        writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
    })

    handler := loggingMiddleware(mux)

    addr := ":8080"
    fmt.Printf("서버 시작: http://localhost%s\n", addr)
    log.Fatal(http.ListenAndServe(addr, handler))
}

9. 퀴즈

퀴즈 1: 슬라이스 동작 이해

다음 코드의 출력 결과는?

a := []int{1, 2, 3, 4, 5}
b := a[1:3]
b[0] = 99
fmt.Println(a)

정답: [1 99 3 4 5]

설명: 슬라이싱으로 만든 ba와 메모리를 공유합니다. b[0]a[1]과 같은 메모리 위치를 가리키므로 b[0] = 99를 하면 a[1]도 99로 변경됩니다.

퀴즈 2: 고루틴과 클로저 함정

다음 코드는 왜 문제가 있고 어떻게 수정해야 하나요?

for i := 0; i < 5; i++ {
    go func() {
        fmt.Println(i)
    }()
}
time.Sleep(time.Second)

정답: 대부분 5를 5번 출력하거나 예측할 수 없는 값을 출력합니다.

설명: 클로저가 변수 i를 캡처할 때 값이 아닌 참조를 캡처합니다. 고루틴이 실행될 때 이미 반복문이 끝나 i가 5가 됩니다. 수정 방법은 두 가지입니다.

1번 방법 - 인수로 전달:

for i := 0; i < 5; i++ {
    go func(n int) {
        fmt.Println(n)
    }(i)
}

2번 방법 - 지역 변수 생성:

for i := 0; i < 5; i++ {
    i := i // 새 변수 i 생성
    go func() {
        fmt.Println(i)
    }()
}
퀴즈 3: 인터페이스와 nil

다음 코드에서 isNil의 반환값은?

type MyError struct{ msg string }
func (e *MyError) Error() string { return e.msg }

func getError() error {
    var err *MyError = nil
    return err
}

func main() {
    err := getError()
    fmt.Println(err == nil)
}

정답: false

설명: Go의 인터페이스는 타입과 값 두 가지 구성요소를 가집니다. getError()*MyError 타입에 nil 값인 인터페이스를 반환합니다. 이 인터페이스는 타입 정보(*MyError)를 가지고 있으므로 error 인터페이스와 비교할 때 nil이 아닙니다. nil error를 올바르게 반환하려면 return nil로 직접 반환해야 합니다.

퀴즈 4: defer 실행 순서

다음 코드의 출력 순서는?

func main() {
    fmt.Println("A")
    defer fmt.Println("defer 1")
    fmt.Println("B")
    defer fmt.Println("defer 2")
    fmt.Println("C")
    defer fmt.Println("defer 3")
    fmt.Println("D")
}

정답: A B C D defer 3 defer 2 defer 1

설명: defer는 LIFO(Last In, First Out) 스택 방식으로 동작합니다. 일반 문장은 선언 순서대로 실행되고, defer된 함수들은 함수 종료 시 역순(나중에 defer된 것이 먼저)으로 실행됩니다.

퀴즈 5: 채널 방향성

다음 함수 시그니처에서 ch chan<- intch <-chan int의 차이는?

정답:

  • chan<- int: 송신 전용 채널 (쓰기만 가능)
  • <-chan int: 수신 전용 채널 (읽기만 가능)

설명: Go는 채널의 방향을 타입으로 제한할 수 있습니다. 이를 통해 함수가 채널을 어떻게 사용하는지 컴파일 타임에 검증할 수 있습니다. 예시:

func send(ch chan<- int, val int) {
    ch <- val  // OK
    // <-ch    // 컴파일 오류
}

func receive(ch <-chan int) int {
    return <-ch  // OK
    // ch <- 1   // 컴파일 오류
}

func pipe(in <-chan int, out chan<- int) {
    for v := range in {
        out <- v * 2
    }
}

채널 방향을 명시하면 실수로 잘못된 방향으로 채널을 사용하는 것을 방지합니다.

Go Language Complete Syntax Guide: From Basics to Concurrency and Generics

Introduction

Go (Golang) is a statically typed, compiled language released by Google in 2009. It is known for its simple syntax, fast compilation, powerful concurrency support, and rich standard library. Go excels at cloud infrastructure, microservices, and CLI tool development.

This guide systematically covers Go's core syntax from start to finish.


1. Go Language Basics

1.1 Variable Declarations

Go offers several ways to declare variables.

package main

import "fmt"

func main() {
    // var keyword with explicit type
    var name string = "Go"
    var age int = 15

    // Type inference with var
    var pi = 3.14159

    // Short declaration (:=) — only inside functions
    greeting := "Hello, Go!"
    count := 42

    // Multiple variable declaration
    var x, y int = 10, 20
    a, b := 100, 200

    // Zero values: uninitialized variables get their type's default value
    var zeroInt int       // 0
    var zeroStr string    // ""
    var zeroBool bool     // false

    fmt.Println(name, age, pi, greeting, count)
    fmt.Println(x, y, a, b)
    fmt.Println(zeroInt, zeroStr, zeroBool)
}

1.2 Constants and iota

package main

import "fmt"

// Simple constants
const Pi = 3.14159
const MaxSize = 1024

// Constant block
const (
    StatusOK       = 200
    StatusNotFound = 404
    StatusError    = 500
)

// iota: auto-incrementing enumeration constants
type Direction int

const (
    North Direction = iota // 0
    East                   // 1
    South                  // 2
    West                   // 3
)

type ByteSize float64

const (
    _           = iota // skip 0 with blank identifier
    KB ByteSize = 1 << (10 * iota) // 1 << 10 = 1024
    MB                              // 1 << 20
    GB                              // 1 << 30
)

func main() {
    fmt.Println(North, East, South, West) // 0 1 2 3
    fmt.Printf("KB=%.0f, MB=%.0f, GB=%.0f\n", float64(KB), float64(MB), float64(GB))
}

1.3 Basic Types

package main

import "fmt"

func main() {
    // Integer types
    var i8 int8 = 127
    var i16 int16 = 32767
    var i32 int32 = 2147483647
    var i64 int64 = 9223372036854775807
    var ui uint = 42

    // Platform-dependent (int64 on 64-bit systems)
    var n int = 100

    // Floating point
    var f32 float32 = 3.14
    var f64 float64 = 3.141592653589793

    // Complex numbers
    var c128 complex128 = 3 + 4i

    // string (UTF-8 encoded, immutable)
    s := "Hello, World"
    fmt.Println(len(s))        // byte count
    fmt.Println([]rune(s))     // rune (Unicode code point) slice

    // byte (alias for uint8)
    var b byte = 'A'

    // rune (alias for int32, Unicode code point)
    var r rune = 'Z'

    // bool
    var flag bool = true

    fmt.Println(i8, i16, i32, i64, ui, n)
    fmt.Println(f32, f64, c128)
    fmt.Println(b, r, flag)
}

1.4 Arrays and Slices

package main

import "fmt"

func main() {
    // Array: fixed size, value type
    var arr [5]int
    arr[0] = 10
    arr2 := [3]string{"Go", "Python", "Rust"}
    arr3 := [...]int{1, 2, 3, 4, 5} // compiler infers size

    fmt.Println(arr, arr2, arr3)

    // Slice: dynamic size, reference type
    s1 := []int{1, 2, 3}
    s2 := make([]int, 3)       // len=3, cap=3
    s3 := make([]int, 3, 10)   // len=3, cap=10

    fmt.Println(len(s1), cap(s1)) // 3, 3
    fmt.Println(len(s2), cap(s2)) // 3, 3
    fmt.Println(len(s3), cap(s3)) // 3, 10

    // append: allocates new array when capacity exceeded
    s1 = append(s1, 4, 5)
    fmt.Println(s1) // [1 2 3 4 5]

    // Slicing (shares underlying memory with original)
    s4 := s1[1:3]  // [2 3]
    s5 := s1[2:]   // [3 4 5]
    s6 := s1[:2]   // [1 2]

    fmt.Println(s4, s5, s6)

    // copy: creates an independent copy
    dst := make([]int, len(s1))
    n := copy(dst, s1)
    fmt.Printf("Copied %d elements: %v\n", n, dst)

    // 2D slice
    matrix := make([][]int, 3)
    for i := range matrix {
        matrix[i] = make([]int, 3)
        for j := range matrix[i] {
            matrix[i][j] = i*3 + j
        }
    }
    fmt.Println(matrix)
}

1.5 Maps

package main

import "fmt"

func main() {
    // Map declaration and initialization
    m1 := map[string]int{
        "apple":  5,
        "banana": 3,
        "cherry": 8,
    }

    m2 := make(map[string][]string)

    // Add/update
    m1["date"] = 12
    m2["fruits"] = []string{"apple", "banana"}

    // Read value
    val := m1["apple"]
    fmt.Println(val) // 5

    // Comma-ok pattern: check key existence
    val2, ok := m1["mango"]
    if !ok {
        fmt.Println("mango not found, zero value:", val2)
    }

    // Delete
    delete(m1, "cherry")
    fmt.Println(m1)

    // Iteration (order not guaranteed)
    for key, value := range m1 {
        fmt.Printf("%s: %d\n", key, value)
    }

    // Writing to a nil map causes a panic
    var m3 map[string]int
    // m3["key"] = 1 // panic: assignment to entry in nil map
    _ = m3
}

1.6 Pointers

package main

import "fmt"

func increment(n *int) {
    *n++ // dereference
}

func newInt(val int) *int {
    return &val // returning address of local variable is fine (moved to heap)
}

func main() {
    x := 42
    p := &x        // address of x
    fmt.Println(p)  // memory address (e.g., 0xc0000b4010)
    fmt.Println(*p) // 42 (dereference)

    *p = 100
    fmt.Println(x) // 100

    increment(&x)
    fmt.Println(x) // 101

    ptr := newInt(200)
    fmt.Println(*ptr) // 200

    // nil pointer
    var nilPtr *int
    fmt.Println(nilPtr) // <nil>
    // fmt.Println(*nilPtr) // panic: nil pointer dereference
}

1.7 Control Structures

package main

import "fmt"

func main() {
    // for: Go's only loop construct
    for i := 0; i < 5; i++ {
        fmt.Print(i, " ")
    }
    fmt.Println()

    // while-style
    n := 1
    for n < 100 {
        n *= 2
    }
    fmt.Println(n)

    // range
    nums := []int{10, 20, 30, 40}
    for idx, val := range nums {
        fmt.Printf("[%d]=%d ", idx, val)
    }
    fmt.Println()

    // Index only
    for i := range nums {
        fmt.Print(nums[i], " ")
    }
    fmt.Println()

    // switch (no automatic fallthrough)
    day := "Monday"
    switch day {
    case "Saturday", "Sunday":
        fmt.Println("Weekend")
    case "Monday":
        fmt.Println("Monday")
    default:
        fmt.Println("Weekday")
    }

    // Switch without condition (replaces if-else chains)
    score := 85
    switch {
    case score >= 90:
        fmt.Println("A")
    case score >= 80:
        fmt.Println("B")
    case score >= 70:
        fmt.Println("C")
    default:
        fmt.Println("F")
    }

    // defer: executed when function exits (LIFO order)
    fmt.Println("start")
    defer fmt.Println("first defer")
    defer fmt.Println("second defer")
    fmt.Println("end")
    // Output: start -> end -> second defer -> first defer
}

2. Functions and Methods

2.1 Multiple Return Values and Named Returns

package main

import (
    "errors"
    "fmt"
)

// Multiple return values
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

// Named return values
func minMax(nums []int) (min, max int) {
    if len(nums) == 0 {
        return // returns zero values
    }
    min, max = nums[0], nums[0]
    for _, n := range nums[1:] {
        if n < min {
            min = n
        }
        if n > max {
            max = n
        }
    }
    return // naked return
}

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

func main() {
    result, err := divide(10, 3)
    if err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Printf("%.4f\n", result) // 3.3333
    }

    _, err = divide(5, 0)
    if err != nil {
        fmt.Println("Error:", err) // division by zero
    }

    min, max := minMax([]int{3, 1, 4, 1, 5, 9, 2, 6})
    fmt.Println(min, max) // 1 9

    fmt.Println(sum(1, 2, 3, 4, 5)) // 15

    // Spread slice as variadic argument
    nums := []int{10, 20, 30}
    fmt.Println(sum(nums...)) // 60
}

2.2 First-Class Functions and Closures

package main

import "fmt"

// Function as argument
func apply(nums []int, fn func(int) int) []int {
    result := make([]int, len(nums))
    for i, n := range nums {
        result[i] = fn(n)
    }
    return result
}

// Function as return value
func multiplier(factor int) func(int) int {
    return func(n int) int {
        return n * factor
    }
}

// Closure: captures variables from outer scope
func counter() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}

func main() {
    double := func(n int) int { return n * 2 }
    nums := []int{1, 2, 3, 4, 5}

    fmt.Println(apply(nums, double))    // [2 4 6 8 10]
    fmt.Println(apply(nums, func(n int) int { return n * n })) // [1 4 9 16 25]

    triple := multiplier(3)
    fmt.Println(triple(7)) // 21

    c1 := counter()
    c2 := counter()
    fmt.Println(c1(), c1(), c1()) // 1 2 3
    fmt.Println(c2(), c2())       // 1 2 (independent state)
}

2.3 Methods (Value Receiver vs Pointer Receiver)

package main

import (
    "fmt"
    "math"
)

type Circle struct {
    Radius float64
}

// Value receiver: operates on a copy (cannot modify original)
func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

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

// Pointer receiver: can modify the original
func (c *Circle) Scale(factor float64) {
    c.Radius *= factor
}

type Rectangle struct {
    Width, Height float64
}

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

func init() {
    fmt.Println("Package initialized")
}

func main() {
    c := Circle{Radius: 5}
    fmt.Printf("Area: %.2f\n", c.Area())
    fmt.Printf("Perimeter: %.2f\n", c.Perimeter())

    c.Scale(2)
    fmt.Printf("Radius after scale: %.1f\n", c.Radius) // 10.0
}

3. Structs and Interfaces

3.1 Structs and Embedding

package main

import "fmt"

type Animal struct {
    Name string
    Age  int
}

func (a Animal) Speak() string {
    return a.Name + " makes a sound"
}

// Embedding: composition over inheritance
type Dog struct {
    Animal       // embedded (anonymous field)
    Breed string
}

func (d Dog) Speak() string {
    return d.Name + " says: Woof!"
}

type ServiceDog struct {
    Dog
    ServiceType string
}

func main() {
    d := Dog{
        Animal: Animal{Name: "Buddy", Age: 3},
        Breed:  "Labrador",
    }

    // Accessing embedded fields directly
    fmt.Println(d.Name)         // same as d.Animal.Name
    fmt.Println(d.Age)
    fmt.Println(d.Breed)
    fmt.Println(d.Speak())         // Dog's Speak()
    fmt.Println(d.Animal.Speak())  // explicit Animal's Speak()

    sd := ServiceDog{
        Dog:         d,
        ServiceType: "Guide",
    }
    fmt.Println(sd.Name)        // sd.Dog.Animal.Name
    fmt.Println(sd.ServiceType)
}

3.2 Interfaces

package main

import (
    "fmt"
    "math"
)

// Interface definition
type Shape interface {
    Area() float64
    Perimeter() float64
}

type Stringer interface {
    String() string
}

type Circle struct{ Radius float64 }
type Rectangle struct{ Width, Height float64 }

func (c Circle) Area() float64      { return math.Pi * c.Radius * c.Radius }
func (c Circle) Perimeter() float64 { return 2 * math.Pi * c.Radius }
func (c Circle) String() string     { return fmt.Sprintf("Circle(r=%.1f)", c.Radius) }

func (r Rectangle) Area() float64      { return r.Width * r.Height }
func (r Rectangle) Perimeter() float64 { return 2 * (r.Width + r.Height) }
func (r Rectangle) String() string {
    return fmt.Sprintf("Rect(%.1fx%.1f)", r.Width, r.Height)
}

// Function accepting an interface
func printShapeInfo(s Shape) {
    fmt.Printf("Area: %.2f, Perimeter: %.2f\n", s.Area(), s.Perimeter())
}

// Empty interface (accepts any type)
func printAny(v interface{}) {
    fmt.Printf("Type: %T, Value: %v\n", v, v)
}

// Type assertion
func describe(i interface{}) {
    if s, ok := i.(string); ok {
        fmt.Printf("string: %q\n", s)
        return
    }
    if n, ok := i.(int); ok {
        fmt.Printf("int: %d\n", n)
        return
    }
    fmt.Printf("unknown type: %T\n", i)
}

// Type switch
func typeSwitch(v interface{}) string {
    switch x := v.(type) {
    case int:
        return fmt.Sprintf("int: %d", x)
    case string:
        return fmt.Sprintf("string: %q", x)
    case bool:
        return fmt.Sprintf("bool: %v", x)
    case []int:
        return fmt.Sprintf("[]int with %d elements", len(x))
    default:
        return fmt.Sprintf("unknown type: %T", x)
    }
}

func main() {
    shapes := []Shape{
        Circle{Radius: 5},
        Rectangle{Width: 4, Height: 6},
    }

    for _, s := range shapes {
        printShapeInfo(s)
        if str, ok := s.(Stringer); ok {
            fmt.Println(str.String())
        }
    }

    printAny(42)
    printAny("hello")
    printAny([]int{1, 2, 3})

    describe("Go language")
    describe(2024)

    fmt.Println(typeSwitch(100))
    fmt.Println(typeSwitch("world"))
    fmt.Println(typeSwitch([]int{1, 2}))
}

4. Error Handling

4.1 Error Types and Custom Errors

package main

import (
    "errors"
    "fmt"
)

// Sentinel errors (package-level error variables)
var (
    ErrNotFound   = errors.New("not found")
    ErrPermission = errors.New("permission denied")
)

// Custom error type
type ValidationError struct {
    Field   string
    Message string
}

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

type DatabaseError struct {
    Code    int
    Message string
    Err     error // wrapped error
}

func (e *DatabaseError) Error() string {
    return fmt.Sprintf("database error [%d]: %s", e.Code, e.Message)
}

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

// Error wrapping with fmt.Errorf
func findUser(id int) error {
    if id <= 0 {
        return fmt.Errorf("findUser: invalid id %d: %w", id, ErrNotFound)
    }
    return nil
}

func getUser(id int) error {
    if err := findUser(id); err != nil {
        return fmt.Errorf("getUser: %w", err)
    }
    return nil
}

func main() {
    // errors.Is: check for a specific error in the chain
    err := getUser(-1)
    if errors.Is(err, ErrNotFound) {
        fmt.Println("not found error:", err)
    }

    // errors.As: extract a specific type from the chain
    dbErr := &DatabaseError{
        Code:    500,
        Message: "connection failed",
        Err:     ErrPermission,
    }
    wrapped := fmt.Errorf("service error: %w", dbErr)

    var de *DatabaseError
    if errors.As(wrapped, &de) {
        fmt.Printf("DB error code: %d\n", de.Code)
    }

    if errors.Is(wrapped, ErrPermission) {
        fmt.Println("permission error found in chain")
    }

    valErr := &ValidationError{Field: "email", Message: "invalid format"}
    fmt.Println(valErr)
}

4.2 panic and recover

package main

import "fmt"

// Recover from panic
func safeDiv(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("recovered from panic: %v", r)
        }
    }()
    return a / b, nil
}

func mustPositive(n int) int {
    if n <= 0 {
        panic(fmt.Sprintf("value must be positive: %d", n))
    }
    return n
}

func main() {
    result, err := safeDiv(10, 2)
    fmt.Printf("10/2 = %d, err = %v\n", result, err)

    result, err = safeDiv(10, 0)
    fmt.Printf("10/0 = %d, err = %v\n", result, err)

    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Panic recovered:", r)
        }
    }()
    mustPositive(-5)
}

5. Goroutines and Channels (Concurrency)

5.1 Goroutines and WaitGroup

package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Printf("Worker %d starting\n", id)
    time.Sleep(time.Millisecond * 100)
    fmt.Printf("Worker %d done\n", id)
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 5; i++ {
        wg.Add(1)
        go worker(i, &wg)
    }

    wg.Wait()
    fmt.Println("All workers completed")
}

5.2 Channels

package main

import (
    "fmt"
    "time"
)

func producer(ch chan<- int, count int) {
    for i := 0; i < count; i++ {
        ch <- i
        time.Sleep(time.Millisecond * 10)
    }
    close(ch)
}

func main() {
    // Unbuffered channel (synchronous)
    ch1 := make(chan int)
    go func() { ch1 <- 42 }()
    fmt.Println(<-ch1) // 42

    // Buffered channel (async until buffer is full)
    ch2 := make(chan string, 3)
    ch2 <- "first"
    ch2 <- "second"
    ch2 <- "third"
    fmt.Println(<-ch2) // first

    // Iterating over a channel
    numbers := make(chan int, 10)
    go producer(numbers, 5)

    for n := range numbers {
        fmt.Print(n, " ")
    }
    fmt.Println()

    // select: handle multiple channels
    ch3 := make(chan string, 1)
    ch4 := make(chan string, 1)

    go func() {
        time.Sleep(time.Millisecond * 50)
        ch3 <- "channel3"
    }()
    go func() {
        time.Sleep(time.Millisecond * 30)
        ch4 <- "channel4"
    }()

    for i := 0; i < 2; i++ {
        select {
        case msg := <-ch3:
            fmt.Println("received:", msg)
        case msg := <-ch4:
            fmt.Println("received:", msg)
        }
    }
}

5.3 Mutex and Synchronization Primitives

package main

import (
    "fmt"
    "sync"
)

type SafeCounter struct {
    mu sync.Mutex
    v  map[string]int
}

func (c *SafeCounter) Inc(key string) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.v[key]++
}

func (c *SafeCounter) Value(key string) int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.v[key]
}

// RWMutex: concurrent reads, exclusive writes
type Cache struct {
    mu   sync.RWMutex
    data map[string]string
}

func (c *Cache) Set(key, value string) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.data[key] = value
}

func (c *Cache) Get(key string) (string, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    val, ok := c.data[key]
    return val, ok
}

// sync.Once: execute exactly once
var (
    instance *Cache
    once     sync.Once
)

func getInstance() *Cache {
    once.Do(func() {
        instance = &Cache{data: make(map[string]string)}
        fmt.Println("Cache instance created")
    })
    return instance
}

func main() {
    counter := SafeCounter{v: make(map[string]int)}
    var wg sync.WaitGroup

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            counter.Inc("key")
        }()
    }
    wg.Wait()
    fmt.Println("Counter:", counter.Value("key")) // 1000

    // sync.Map: concurrency-safe map
    var sm sync.Map
    sm.Store("go", "1.21")
    sm.Store("python", "3.12")

    sm.Range(func(key, value interface{}) bool {
        fmt.Printf("%v: %v\n", key, value)
        return true
    })

    c1 := getInstance()
    c2 := getInstance()
    fmt.Println(c1 == c2) // true (same instance)
}

5.4 context.Context

package main

import (
    "context"
    "fmt"
    "time"
)

func doWork(ctx context.Context, id int) error {
    select {
    case <-time.After(time.Millisecond * 200):
        fmt.Printf("Work %d completed\n", id)
        return nil
    case <-ctx.Done():
        fmt.Printf("Work %d cancelled: %v\n", id, ctx.Err())
        return ctx.Err()
    }
}

func fetchData(ctx context.Context, url string) (string, error) {
    if requestID, ok := ctx.Value("requestID").(string); ok {
        fmt.Printf("Request ID: %s, URL: %s\n", requestID, url)
    }
    select {
    case <-time.After(time.Millisecond * 100):
        return "data from " + url, nil
    case <-ctx.Done():
        return "", ctx.Err()
    }
}

func main() {
    // WithCancel
    ctx, cancel := context.WithCancel(context.Background())
    go func() {
        time.Sleep(time.Millisecond * 50)
        cancel()
    }()
    doWork(ctx, 1)

    // WithTimeout
    ctx2, cancel2 := context.WithTimeout(context.Background(), time.Millisecond*150)
    defer cancel2()
    doWork(ctx2, 2)

    // WithDeadline
    deadline := time.Now().Add(time.Millisecond * 300)
    ctx3, cancel3 := context.WithDeadline(context.Background(), deadline)
    defer cancel3()
    doWork(ctx3, 3)

    // WithValue: pass request-scoped data
    ctx4 := context.WithValue(context.Background(), "requestID", "req-001")
    data, err := fetchData(ctx4, "https://api.example.com/users")
    if err == nil {
        fmt.Println("Data:", data)
    }
}

6. Packages and Modules

6.1 Go Modules

# Initialize a new module
go mod init github.com/username/myapp

# Add a dependency
go get github.com/gin-gonic/gin@v1.9.1

# Tidy dependencies
go mod tidy

# Create vendor directory
go mod vendor

Example go.mod file:

module github.com/username/myapp

go 1.21

require (
    github.com/gin-gonic/gin v1.9.1
    github.com/stretchr/testify v1.8.4
)

6.2 Package Structure Best Practices

myapp/
├── cmd/
│   └── server/
│       └── main.go          # entry point
├── internal/                 # not importable by external packages
│   ├── handler/
│   │   └── user.go
│   ├── service/
│   │   └── user.go
│   └── repository/
│       └── user.go
├── pkg/                     # importable library code
│   └── validator/
│       └── validator.go
├── api/                     # API definitions (OpenAPI, Protobuf)
├── config/
│   └── config.go
├── go.mod
└── go.sum

6.3 Key Standard Library Packages

package main

import (
    "fmt"
    "os"
    "strings"
    "strconv"
    "time"
    "io"
    "bytes"
)

func main() {
    // strings package
    s := "  Hello, Go World!  "
    fmt.Println(strings.TrimSpace(s))
    fmt.Println(strings.ToUpper(s))
    fmt.Println(strings.Contains(s, "Go"))
    fmt.Println(strings.Replace(s, "Go", "Golang", 1))
    parts := strings.Split("a,b,c,d", ",")
    fmt.Println(parts)
    fmt.Println(strings.Join(parts, " | "))

    // strconv package
    n, _ := strconv.Atoi("42")
    fmt.Println(n + 1)
    str := strconv.Itoa(100)
    fmt.Println(str + " bottles")
    f, _ := strconv.ParseFloat("3.14", 64)
    fmt.Printf("%.2f\n", f)

    // time package
    now := time.Now()
    fmt.Println(now.Format("2006-01-02 15:04:05"))
    fmt.Println(now.Format(time.RFC3339))
    tomorrow := now.Add(24 * time.Hour)
    fmt.Println(tomorrow.Sub(now))

    t, _ := time.Parse("2006-01-02", "2026-03-17")
    fmt.Println(t.Year(), t.Month(), t.Day())

    // os package
    fmt.Println(os.Getenv("HOME"))

    // io and bytes
    buf := bytes.NewBufferString("Hello")
    buf.WriteString(", World")
    data, _ := io.ReadAll(buf)
    fmt.Println(string(data))
}

7. Generics (Go 1.18+)

7.1 Generic Functions and Types

package main

import "fmt"

// Generic function with type parameter
func Min[T interface{ ~int | ~float64 | ~string }](a, b T) T {
    if a < b {
        return a
    }
    return b
}

func Map[T, 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, pred func(T) bool) []T {
    var result []T
    for _, v := range slice {
        if pred(v) {
            result = append(result, v)
        }
    }
    return result
}

func Reduce[T, U any](slice []T, initial U, fn func(U, T) U) U {
    acc := initial
    for _, v := range slice {
        acc = fn(acc, v)
    }
    return acc
}

// Generic type
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) {
    var zero T
    if len(s.items) == 0 {
        return zero, false
    }
    top := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return top, true
}

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

// comparable constraint: allows == operator
func Contains[T comparable](slice []T, item T) bool {
    for _, v := range slice {
        if v == item {
            return true
        }
    }
    return false
}

// Custom type constraint
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
}

func main() {
    fmt.Println(Min(3, 5))         // 3
    fmt.Println(Min(3.14, 2.71))   // 2.71
    fmt.Println(Min("apple", "banana")) // apple

    nums := []int{1, 2, 3, 4, 5}
    doubled := Map(nums, func(n int) int { return n * 2 })
    fmt.Println(doubled) // [2 4 6 8 10]

    strs := Map(nums, func(n int) string { return fmt.Sprintf("item_%d", n) })
    fmt.Println(strs)

    evens := Filter(nums, func(n int) bool { return n%2 == 0 })
    fmt.Println(evens) // [2 4]

    total := Reduce(nums, 0, func(acc, n int) int { return acc + n })
    fmt.Println(total) // 15

    var s Stack[string]
    s.Push("first")
    s.Push("second")
    s.Push("third")

    for s.Len() > 0 {
        item, _ := s.Pop()
        fmt.Println(item)
    }

    fmt.Println(Contains([]string{"go", "python", "rust"}, "go")) // true
    fmt.Println(Sum([]float64{1.1, 2.2, 3.3})) // 6.6
}

8. Code Example: REST API Server (net/http)

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "strconv"
    "strings"
    "sync"
)

type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

type UserStore struct {
    mu     sync.RWMutex
    users  map[int]User
    nextID int
}

func NewUserStore() *UserStore {
    return &UserStore{
        users:  make(map[int]User),
        nextID: 1,
    }
}

func (s *UserStore) Create(name, email string) User {
    s.mu.Lock()
    defer s.mu.Unlock()
    user := User{ID: s.nextID, Name: name, Email: email}
    s.users[s.nextID] = user
    s.nextID++
    return user
}

func (s *UserStore) GetAll() []User {
    s.mu.RLock()
    defer s.mu.RUnlock()
    users := make([]User, 0, len(s.users))
    for _, u := range s.users {
        users = append(users, u)
    }
    return users
}

func (s *UserStore) GetByID(id int) (User, bool) {
    s.mu.RLock()
    defer s.mu.RUnlock()
    u, ok := s.users[id]
    return u, ok
}

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

func makeUsersHandler(store *UserStore) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        switch r.Method {
        case http.MethodGet:
            users := store.GetAll()
            writeJSON(w, http.StatusOK, users)

        case http.MethodPost:
            var body struct {
                Name  string `json:"name"`
                Email string `json:"email"`
            }
            if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
                writeJSON(w, http.StatusBadRequest, map[string]string{
                    "error": "invalid request body",
                })
                return
            }
            user := store.Create(body.Name, body.Email)
            writeJSON(w, http.StatusCreated, user)

        default:
            http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
        }
    }
}

func makeUserHandler(store *UserStore) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        parts := strings.Split(r.URL.Path, "/")
        if len(parts) < 4 {
            http.Error(w, "Bad Request", http.StatusBadRequest)
            return
        }
        id, err := strconv.Atoi(parts[3])
        if err != nil {
            http.Error(w, "Invalid ID", http.StatusBadRequest)
            return
        }

        user, ok := store.GetByID(id)
        if !ok {
            writeJSON(w, http.StatusNotFound, map[string]string{
                "error": fmt.Sprintf("user %d not found", id),
            })
            return
        }
        writeJSON(w, http.StatusOK, user)
    }
}

func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        log.Printf("%s %s", r.Method, r.URL.Path)
        next.ServeHTTP(w, r)
    })
}

func main() {
    store := NewUserStore()
    store.Create("Alice", "alice@example.com")
    store.Create("Bob", "bob@example.com")

    mux := http.NewServeMux()
    mux.Handle("/api/users", makeUsersHandler(store))
    mux.Handle("/api/users/", makeUserHandler(store))
    mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
        writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
    })

    handler := loggingMiddleware(mux)

    addr := ":8080"
    fmt.Printf("Server started: http://localhost%s\n", addr)
    log.Fatal(http.ListenAndServe(addr, handler))
}

9. Quizzes

Quiz 1: Slice Behavior

What does the following code print?

a := []int{1, 2, 3, 4, 5}
b := a[1:3]
b[0] = 99
fmt.Println(a)

Answer: [1 99 3 4 5]

Explanation: The slice b created by slicing shares the underlying memory with a. b[0] points to the same memory location as a[1], so assigning 99 to b[0] also changes a[1] to 99. Use copy if you need an independent slice.

Quiz 2: Goroutine and Closure Trap

Why is the following code problematic and how should it be fixed?

for i := 0; i < 5; i++ {
    go func() {
        fmt.Println(i)
    }()
}
time.Sleep(time.Second)

Answer: It most likely prints 5 five times (or some unpredictable values).

Explanation: The closure captures the variable i by reference, not by value. By the time the goroutines execute, the loop has already finished and i equals 5. Two correct approaches:

Approach 1 — pass as argument:

for i := 0; i < 5; i++ {
    go func(n int) {
        fmt.Println(n)
    }(i)
}

Approach 2 — shadow with a new variable:

for i := 0; i < 5; i++ {
    i := i // new variable i
    go func() {
        fmt.Println(i)
    }()
}
Quiz 3: Interface and nil

What does err == nil print in the following code?

type MyError struct{ msg string }
func (e *MyError) Error() string { return e.msg }

func getError() error {
    var err *MyError = nil
    return err
}

func main() {
    err := getError()
    fmt.Println(err == nil)
}

Answer: false

Explanation: A Go interface value has two components: type and value. getError() returns an interface that holds the type *MyError with a nil value. Because the interface has a non-nil type component, it is not equal to the nil interface. To correctly return a nil error, return nil directly instead of a typed nil pointer.

Quiz 4: defer Execution Order

What is the output order of the following code?

func main() {
    fmt.Println("A")
    defer fmt.Println("defer 1")
    fmt.Println("B")
    defer fmt.Println("defer 2")
    fmt.Println("C")
    defer fmt.Println("defer 3")
    fmt.Println("D")
}

Answer: A B C D defer 3 defer 2 defer 1

Explanation: defer uses a LIFO (Last In, First Out) stack. Normal statements execute in declaration order. Deferred functions run in reverse order when the function exits — the last deferred runs first.

Quiz 5: Channel Directionality

What is the difference between ch chan<- int and ch <-chan int?

Answer:

  • chan<- int: send-only channel (write only)
  • <-chan int: receive-only channel (read only)

Explanation: Go allows you to restrict channel direction in the type system. This lets the compiler verify at compile time how a function uses a channel. Example:

func send(ch chan<- int, val int) {
    ch <- val   // OK
    // <-ch     // compile error
}

func receive(ch <-chan int) int {
    return <-ch  // OK
    // ch <- 1   // compile error
}

func pipe(in <-chan int, out chan<- int) {
    for v := range in {
        out <- v * 2
    }
}

Specifying channel direction prevents accidentally using a channel in the wrong direction.