LLM WikiAccess-protected knowledge portal
← 스터디 홈
1편 · 약 21분

Go 기초 문법과 설계 철학

Go는 왜 만들어졌는가

2009년 Google 내부에서 Rob Pike, Ken Thompson, Robert Griesemer가 설계한 Go는 C/C++의 속도와 안전성을 유지하면서도 대규모 팀이 협업하기 쉬운 언어를 목표로 탄생했다. 2012년 Rob Pike는 "Go at Google" 발표에서 이 목표를 명확히 했다: 빌드 속도, 가독성, 동시성, 그리고 코드베이스의 장기적 유지보수성.

Go가 인프라 도구의 사실상 표준 언어가 된 이유도 여기에 있다. Kubernetes, Docker, Terraform, Prometheus, Etcd 모두 Go로 작성되었다. 이 도구들이 공통적으로 요구하는 특성 — 빠른 빌드, 낮은 메모리 사용, 뛰어난 동시성, 단일 바이너리 배포 — 이 Go의 설계 목표와 정확히 일치한다.

단순성
(Simplicity)
명시성
(Explicitness)
동시성
(Concurrency)
구성 가능성
(Composition)
Kubernetes Docker Terraform Prometheus Etcd gRPC-Go
Go의 핵심 설계 원칙과 인프라 생태계

설계 철학: 단순함은 전략이다

Go의 가장 중요한 특징은 의도적으로 기능을 줄인 것이다. 상속 없음, 제네릭은 Go 1.18에서야 추가, 예외(exception) 없음, 연산자 오버로딩 없음. 이 선택들은 모두 코드 가독성과 팀 협업을 위한 트레이드오프다.

Rob Pike의 말: "기능이 없는 것이 Go의 기능이다." 코드가 명시적이고 한 가지 방식으로만 작성되도록 유도한다.

Go Proverbs에서 주요 철학 발췌:

  • "명시적이 암묵적보다 낫다"
  • "에러는 값이다 (Errors are values)"
  • "인터페이스를 크게 만들지 마라"
  • "메모리를 공유하여 통신하지 말고, 통신하여 메모리를 공유하라"

기본 문법 구조

Go 프로그램은 반드시 package 선언으로 시작한다. 실행 가능한 프로그램은 package main이어야 한다.

package main

import (
    "fmt"
    "time"
)

func main() {
    fmt.Println("Hello, Go!")
    fmt.Println("현재 시각:", time.Now().Format("2006-01-02"))
}

Go는 미사용 import와 미사용 변수를 컴파일 에러로 처리한다. 이것이 불편하게 느껴질 수 있지만, 팀 코드베이스에서 불필요한 의존성과 죽은 코드를 예방하는 강력한 메커니즘이다.

변수 선언과 제로값(Zero Value)

Go는 선언과 동시에 초기화되지 않은 변수에 제로값(Zero Value) 을 자동으로 부여한다.

var i int       // 0
var s string    // ""
var b bool      // false
var p *int      // nil

:= 단축 선언은 함수 내에서 타입을 추론하며 변수를 선언한다.

name := "Alice"          // string 추론
count := 42              // int 추론
pi := 3.14159            // float64 추론

제로값의 철학은 중요하다. Go의 자료구조들은 초기화 없이도 안전하게 사용할 수 있도록 설계되었다. sync.Mutex는 선언만 해도 즉시 사용 가능한 뮤텍스다.

함수: 다중 반환값

Go 함수는 여러 값을 반환할 수 있다. 이것이 Go 에러 처리 패턴의 근본이다.

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

result, err := divide(10, 3)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("결과: %.2f\n", result)

이름 있는 반환값(Named Return Values)으로 문서화 역할을 겸하거나, 빈 반환문(return)을 쓸 수 있다.

func minMax(nums []int) (min, max int) {
    min, max = nums[0], nums[0]
    for _, n := range nums[1:] {
        if n < min { min = n }
        if n > max { max = n }
    }
    return // 명시적으로 min, max 반환
}

슬라이스(Slice): Go의 핵심 컬렉션

배열(Array)은 크기가 고정이지만, 슬라이스는 동적 배열이다. 슬라이스는 내부적으로 세 개의 필드를 갖는다: 기저 배열을 가리키는 포인터, 길이(len), 용량(cap).

// 슬라이스 생성
nums := []int{1, 2, 3, 4, 5}
fruits := make([]string, 0, 10)  // len=0, cap=10

// 요소 추가
fruits = append(fruits, "apple", "banana")

// 슬라이싱
sub := nums[1:4]  // [2 3 4] — 기저 배열을 공유함
슬라이스 헤더 ptr → len = 5 cap = 8
기저 배열 (heap)
1 2 3 4 5 _ _ _
슬라이스[1:4]는 같은 기저 배열의 인덱스 1~3을 가리키는 새 헤더를 만든다.
슬라이스 내부 구조

슬라이스의 부분 슬라이스는 기저 배열을 공유한다. 한쪽 수정이 다른 쪽에 영향을 줄 수 있으므로, 독립적인 복사본이 필요하면 copy(dst, src)를 명시적으로 호출한다.

맵(Map)

맵은 해시 테이블 기반 키-값 구조다. 내부적으로 해시맵에 대한 포인터이므로, 함수에 맵을 넘기면 참조가 공유된다.

ages := map[string]int{
    "Alice": 30,
    "Bob":   25,
}

// 존재 확인 (two-value idiom)
age, ok := ages["Charlie"]
if !ok {
    fmt.Println("Charlie는 없음")
}

// 삭제
delete(ages, "Bob")

초기화되지 않은 (var m map[string]int) 맵에 쓰기를 하면 패닉이 발생한다. 항상 make(map[K]V) 또는 리터럴로 초기화해야 한다.

구조체와 메서드

Go는 클래스 대신 구조체(struct)메서드로 데이터와 동작을 묶는다. 상속 없이 구성(composition) 으로 재사용을 달성한다.

type User struct {
    ID    int
    Name  string
    Email string
}

// 값 수신자 — 복사본에서 동작
func (u User) Greet() string {
    return "안녕하세요, " + u.Name + "님"
}

// 포인터 수신자 — 원본 수정 가능
func (u *User) UpdateEmail(email string) {
    u.Email = email
}

값 수신자 vs 포인터 수신자 선택 기준:

  • 구조체를 수정해야 하면 포인터 수신자
  • 구조체가 크다면 복사 비용 때문에 포인터 수신자
  • 작은 읽기 전용 연산이면 값 수신자도 무방

구조체 임베딩으로 상속처럼 동작을 재사용한다.

type Admin struct {
    User              // User의 모든 필드와 메서드를 포함
    Permissions []string
}

admin := Admin{User: User{Name: "관리자"}, Permissions: []string{"read", "write"}}
fmt.Println(admin.Greet())  // User의 메서드를 직접 호출 가능

포인터: 명시적인 참조

Go는 포인터를 지원하지만 포인터 연산은 없다. &는 주소를 얻고, *는 역참조한다.

x := 42
p := &x    // p는 *int 타입, x의 주소를 가짐
*p = 100   // x가 100으로 바뀜
fmt.Println(x) // 100

함수에 구조체를 값으로 넘기면 복사가 일어난다. 큰 구조체를 자주 수정하거나 같은 인스턴스를 여러 곳에서 공유해야 한다면 포인터를 전달한다. Go GC가 포인터가 살아있는 한 대상을 자동으로 관리하므로 C처럼 수동 해제할 필요가 없다.

에러 처리: 예외 없이 명시적으로

Go에는 try-catch가 없다. 에러는 함수의 마지막 반환값으로 명시적으로 반환한다.

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 {
        return &ValidationError{Field: "age", Message: "음수는 불가"}
    }
    if age > 150 {
        return &ValidationError{Field: "age", Message: "너무 큰 값"}
    }
    return nil
}

err := validateAge(-1)
if err != nil {
    var ve *ValidationError
    if errors.As(err, &ve) {
        fmt.Printf("유효성 오류: 필드=%s\n", ve.Field)
    }
}

errors.Aserrors.Is 는 Go 1.13에서 추가된 에러 래핑 처리 함수다. fmt.Errorf("처리 중 오류: %w", err)로 에러를 래핑하면 원본 에러를 유지하면서 컨텍스트를 추가할 수 있다.

defer, panic, recover

defer 는 현재 함수가 반환될 때 실행할 코드를 등록한다. 파일 닫기, 락 해제, 트랜잭션 롤백 같은 정리(cleanup) 로직에 쓴다.

func readFile(path string) ([]byte, error) {
    f, err := os.Open(path)
    if err != nil {
        return nil, err
    }
    defer f.Close()  // 어떤 경로로 반환되든 실행됨

    return io.ReadAll(f)
}

여러 defer는 LIFO(후입선출) 순서로 실행된다.

panic 은 복구 불가능한 상태에서 프로그램 흐름을 중단시킨다. recover 는 defer 안에서만 유효하며, 진행 중인 panic을 잡아 정상 흐름으로 복구한다.

func safeCall(fn func()) (err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("panic 발생: %v", r)
        }
    }()
    fn()
    return nil
}

일반적인 에러 처리에는 panic을 쓰지 않는다. 패닉은 "절대 발생해서는 안 되는 프로그래밍 오류" — 예를 들어 nil 포인터 역참조나 슬라이스 범위 초과 — 에 국한한다.

패키지와 모듈 시스템

Go는 디렉토리가 곧 패키지다. 외부에 노출할 식별자는 대문자로 시작한다(exported). 소문자 시작은 패키지 내부 전용이다.

// user/user.go
package user

type User struct { // 외부 공개
    ID   int
    Name string
}

type secret struct { // 패키지 내부 전용
    token string
}

Go 1.11부터 modules가 표준이다. go.mod 파일이 프로젝트의 모듈 이름과 의존성을 관리한다.

module github.com/myorg/myapp

go 1.23

require (
    google.golang.org/grpc v1.65.0
    github.com/go-redis/redis/v9 v9.5.0
)

go get, go mod tidy, go build가 의존성 다운로드와 빌드를 처리한다. 의존성을 vendor/ 디렉토리에 직접 포함시킬 수도 있어 오프라인 빌드와 재현 가능한 빌드를 보장한다.

Go 1.25 시점의 언어 현황

2025년 8월 Go 1.25가 출시되며 언어는 여전히 "조용하고 신중한 개선" 원칙을 유지하고 있다. Go 1.18에서 추가된 제네릭은 이제 표준 라이브러리 (slices, maps, cmp 패키지)에 자연스럽게 스며들었다. slog 구조화 로깅은 1.21에서 표준 라이브러리로 편입되었고, 1.24에서는 iter 패키지가 추가되어 커스텀 이터레이터를 지원한다.

Go의 강점은 "언어 자체가 10년 뒤에도 크게 달라지지 않는다"는 안정성이다. Go 1.0 코드가 Go 1.25에서 수정 없이 빌드된다.

References