An intro to Go for non-Go developers

Ben Hoyt

Go를 모르는 개발자를 위한 Go 입문

원문은 Ben Hoyt님이 에 게재했습니다. 이 블로그 구독하기

요약: Go를 처음 접하는 개발자들을 대상으로 Go 입문 발표를 여러 차례 진행한 바 있다. 이 글은 그 발표 내용을 기술 아티클로 풀어낸 것이다. Go를 왜 써야 하는지 살펴보고, 표준 라이브러리와 언어 자체에 대한 간략한 개요를 제공한다.

몇 년 전 필자는 사이드 프로젝트인 Gifty Weddings의 서버를 Python에서 Go로 포팅하면서 Go를 배웠다. 언어를 배우는 재미있는 방식이었고, 버스 통근 시간을 활용해 약 “2주” 만에 Go를 기초 수준에서 익히고 코드를 포팅할 수 있었다.

그 이후로 필자는 이 언어로 작업하는 것을 진심으로 즐겨왔고, 직장뿐 아니라 GoAWKzztgo 같은 사이드 프로젝트에서도 광범위하게 사용해 왔다. 현재 직장인 Compass.com에서 Go 사용량은 필자가 재직한 기간 동안 크게 늘었는데, 200개가 넘는 서비스 중 절반 정도가 Go로 작성되어 있다.

이 글에서는 필자가 생각하는 Go의 장점 몇 가지를 설명하고, 표준 라이브러리를 아주 간략하게 개요한 뒤 핵심 언어를 깊이 살펴본다. 다만 실제 Go 코드가 어떤 모습인지 바로 감을 잡고 싶다면 HTTP 서버 예제로 건너뛰어도 좋다.

왜 Go인가?

다음 Google Trends 차트에서 보듯, Go는 지난 몇 년간 매우 인기를 얻었는데, 부분적으로는 언어 자체의 단순성 덕분이지만, 어쩌면 더 중요한 이유는 훌륭한 툴링 때문이다.

2010년부터 2020년까지 "golang"에 대한 Google Trends 데이터

필자가 Go 프로그래밍을 즐기는 이유이자, 여러분도 좋아할 만한 이유는 다음과 같다:

  • 작고 단순한 핵심 언어. Go는 규모 면에서 C와 비슷하게 느껴지며, 읽기 쉬운 언어 명세가 약 50페이지에 불과하다(Java 명세의 770페이지와 비교하면). 덕분에 배우거나 다른 사람에게 가르치기 쉽다.
  • 고품질의 표준 라이브러리, 특히 서버와 네트워크 작업을 위한 라이브러리가 뛰어나다. 자세한 내용은 아래에서 다룬다.
  • 일급 동시성 지원 — 스레드와 비슷하지만 더 가벼운 고루틴(goroutine)과 고루틴을 시작하는 go 키워드, 고루틴 간 통신을 위한 채널(channel), 그리고 이 모든 것을 조율하는 스케줄러를 갖춘 런타임.
  • 네이티브 코드로 컴파일되어 모든 주요 플랫폼에서 배포하기 쉬운 바이너리를 생성한다.
  • 가비지 컬렉션은 별도의 튜닝 없이도 동작하며(낮은 지연 시간에 최적화되어 있다).
  • 정적 타입 언어이지만 타입 추론을 제공해 불필요한 “타입 반복”을 줄여준다.
  • 훌륭한 문서 — 간결하면서 실행 가능한 예제가 풍부하다.
  • 탁월한 툴링. 프로젝트를 빌드하려면 go build, 테스트를 찾아 실행하려면 go test라고 입력하기만 하면 된다. CPU 및 메모리 프로파일링, 코드 커버리지, 크로스 컴파일까지 외부 도구 없이 모두 가능하다.
  • 빠른 컴파일 속도. Go는 처음부터 빠른 컴파일을 염두에 두고 설계되었다. 실제로 공동 창시자인 Rob Pike는 “Go는 거대한 [C++] 컴파일을 기다리다가 구상되었다”고 농담하기도 했다.
  • 매우 안정적인 언어와 라이브러리를 제공하며, 모든 Go 1 프로그램이 이후 Go 1.x 버전에서도 변경 없이 동작한다는 엄격한 호환성 약속을 지킨다.
  • 선호도 높음. StackOverflow 2019년 조사에 따르면 세 번째로 가장 배우고 싶은 프로그래밍 언어로 꼽혔으므로, Go를 쓰고 싶어 하는 개발자를 채용하기도 쉽다.
  • 클라우드 도구에 널리 사용. Docker와 Kubernetes가 Go로 작성되었으며, Dropbox, Digital Ocean, Cloudflare 등 수많은 기업이 Go를 광범위하게 사용하고 있다.

표준 라이브러리

Go의 표준 라이브러리는 방대하고 크로스 플랫폼을 지원하며 문서화가 잘 되어 있다. Python과 마찬가지로 Go도 “배터리 포함(batteries included)”이라, 서드파티 의존성 없이도 바로 유용한 서버와 CLI 도구를 만들 수 있다. 다음은 주요 하이라이트이다(필자가 사용해 본 것 위주로 편향되어 있다):

서드파티 패키지에 관한 한, Go의 전형적인 철학은 npm 패키지를 마구 끌어다 쓰는 JavaScript 방식과 거의 정반대다. Google Go 팀의 테크 리드인 Russ Cox는 소프트웨어 의존성 문제에 대해 이야기하며, Go 공동 창시자인 Rob Pike는 “약간의 의존성보다 약간의 복사가 낫다”고 말하곤 한다. 따라서 대다수의 Gopher(Go 개발자)는 서드파티 라이브러리 사용에 상당히 보수적이라고 해도 과언이 아니다.

다만 필자가 이 발표를 처음 작성한 이후, Go 팀은 의존성을 어떻게 관리하고 버전을 고정해야 하는지에 대한 공식적인 해답인 modules를 설계하고 구축했다. 직접 써 보니 사용성이 좋았고, 기존의 일반적인 go 하위 명령들과도 잘 동작한다.

언어 기능

이제 Go 자체가 어떻게 생겼는지 본격적으로 살펴보고, 언어를 차근차근 훑어보자.

Hello world

Go는 C와 유사한 문법을 가지고 있으며, 중괄호는 필수이고 세미콜론은 없다(형식 문법 제외). 프로젝트는 import와 패키지를 통해 구조화되는데, 패키지는 하나 이상의 .go 파일로 이루어진 디렉터리가 컴파일 단위가 된다. “hello world”는 다음과 같이 생겼다:

package main

import "fmt"

func main() {
    fmt.Println("Hello, world!")
}

다소 논란이 있는 기능들

Go에는 처음 보면 다소 꺼려지는 요소가 몇 가지 있지만, 익숙해지면 꽤 괜찮다고 느껴지는 것들이다.

첫 번째는 코드 포매팅이다. go fmt를 실행하기만 하면 중괄호와 공백(그리고 탭!)을 있어야 할 위치에 알아서 맞춰준다. 스타일 논쟁을 피하고 일관되게 포매팅된 코드로 바로 작업에 집중할 수 있는 훌륭한 방법이다.

대문자로 시작하는 이름은 public(“exported”), 소문자로 시작하는 이름은 패키지 내부에서만 보이는 private이다. 처음에는 매우 낯설게 느껴지지만 규칙 자체는 이해하기 쉽고, Java스러운 public static void 같은 키워드 잡음을 줄여준다. public 키워드 자체가 필요 없다. 다음은 그 예이다:

package people

type Person struct {
    Name      string // fields Name and Age are exported ("public")
    Age       int
    hairColor color  // hairColor is not exported ("private")
}

func New() *Person { ... } // New is exported

func doThing() { ... } // doThing is not exported

또 일부 개발자들이 의아해하는 점은 경고가 곧 오류라는 것이다. Go의 내장 도구는 옵션이 거의 없으며, 다른 컴파일러에서는 경고에 그칠 것들이 Go에서는 오류가 된다(다시 말해 경고라는 개념이 없다).

따라서 사용되지 않는 지역 변수나 import 같은 것도 컴파일 오류가 된다. 개발 중에는 약간 성가실 수 있지만, 코드를 깨끗하게 유지하고 어떤 컴파일러 경고를 켤지 두고 개발자들끼리 논쟁하는 일을 방지한다.

상당히 논란이 많은 기능들

더 논란이 많은 기능들 — 혹은 오히려 기능의 부재 — 도 몇 가지 있는데, 바로 Go에 예외가 없다는 점과 사용자 정의 제네릭이 없다는 점이다.

Go에는 전통적인 의미의 예외가 없다. 처음부터 “오류는 값이다(errors are values)”라는 철학 아래, 오류를 다른 값처럼 명시적으로 전달하고 반환하며 처리해야 한다는 것이 원칙이었다. 따라서 FileNotFound 예외를 발생시키는 대신 오류 값을 검사한다:

f, err := os.Open("filename.ext")
if err != nil {
    log.Fatal(err)
}
// do something with the open file f

이 때문에 코드는 다소 장황해지지만(if err != nil을 단축 다이얼에 등록해 두자), 각 단계에서 오류 처리를 명시적으로 만든다는 장점이 있다. 문맥을 추가하거나, 오류를 로그로 남기거나, 더 상위 수준의 오류 객체로 변환하거나, 심지어 무시할 수도 있지만, 어쨌든 명시적으로 처리해야 한다.

Go가 없다고 자주 비판받는 두 번째는 사용자 정의 제네릭이다. 따라서 타입 안전한 OrderedMap<int> 같은 것을 직접 정의할 수 없다. 하지만 내장된 slicemap 타입에 대해서는 정적 타입 제네릭을 제공하므로, 큰 불편 없이도 상당히 멀리 갈 수 있다.

또 한 가지 짚고 넘어갈 점은 제네릭이 현재 작업 중이라는 것이다. Go 팀은 제네릭을 억지로 덧붙이기보다, 비용을 신중히 따져 Go다운 방식으로 추가하려고 한다. 초안 제안실험적 구현, 그리고 Featherweight Go라는 최근 타입 이론 논문까지 나와 있다. 따라서 향후 12~18개월 안에 어떤 형태의 제네릭이 Go에 포함되어도 전혀 놀랍지 않을 것이다.

좋다, Go에 없는 것에 대한 이야기는 이 정도로 하고, Go가 실제로 가지고 있는 기능들(그 중 상당수는 독특하다)을 살펴보자.

간결한 타입 추론

Go는 := 연산자를 이용한 변수 선언에서 간결한 타입 추론을 제공하는데, 이를 “짧은 변수 선언(short variable declarations)”이라고 한다. 타입 추론 덕분에 타이핑이 줄어들어 스크립트 언어 같은 느낌이 난다. 다음은 그 예이다:

package main

import "fmt"

// Output: 3 4 hello 5
func main() {
    var i int = 3
    j := 4          // j is an int
    s := "hello"    // s is a string
    a := add(2, 3)  // a is an int
    fmt.Println(i, j, s, a)
}

func add(x, y int) int {
    return x + y
}

반면 Go는 서로 다른 크기나 부호를 가진 정수 사이에서도 자동 타입 변환을 전혀 수행하지 않는다. C와의 차이를 설명하는 FAQ 답변을 인용하면 다음과 같다:

C에서 숫자 타입 간의 자동 변환이 주는 편의성은 그것이 야기하는 혼란에 비하면 사소하다. 표현식은 언제 unsigned인가? 값은 얼마나 큰가? 오버플로가 발생하는가? 결과는 실행되는 머신과 무관하게 이식성이 있는가?

For 루프와 range

Go에는 단 하나의 루프 키워드인 for가 있으며, 이는 while 루프, 전통적인 C 스타일 루프, 그리고 range 루프(Go의 “for each”)에 모두 사용된다. slice나 map을 range로 순회할 때 첫 번째 항목으로 인덱스(또는 map 키)를, 두 번째 항목으로 값을 제공한다.

다음은 몇 가지 루프 예제인데, 지금까지는 특별히 낯선 것은 없다:

// C style "for"
for i := 0; i < 10; i++ {
    fmt.Println(i)
}

// Like "while"
for safe.IsLocked() {
    time.Sleep(5 * time.Second)
}

// Loop through elements of array or slice
for index, person := range people {
    fmt.Println(index, person)
}

// If you don't care about the index
for _, person := range people { ... }

// Loop through keys/values of a map
for word, count := range counts { ... }

Slice는 참 좋다

Go에서 slice는 배열의 일부에 대한 참조이며, 내부 표현은 매우 단순하다. 데이터 포인터, 길이, 용량(capacity) 세 가지다. slice는 제네릭하므로 []float64로 표기되는 float64 slice나 []Person으로 표기되는 Person 구조체 slice를 가질 수 있다.

Python과 유사한 문법으로 slice를 “슬라이싱”할 수 있다. 예를 들어 slice[:5]는 앞의 5개 요소를 바라보는 새로운 slice를 반환한다. 새로운 slice는 여전히 같은 backing array를 참조하므로 포인터를 다루는 만큼 효율적이지만 메모리 안전성을 보장한다. 런타임이 slice 끝을 벗어나거나 다른 위험한 동작을 하는 것을 막아준다.

내장 제네릭 함수인 append()는 slice에 단일 요소를 추가하고 새로운 slice를 반환한다. backing array(용량)가 충분히 크지 않으면 두 배 크기의 새로운 배열을 할당하고 요소를 복사한다.

slice는 다음과 같이 생겼다:

// Create array and slice pointing into it
nums := []int{3, 4, 5, 6}

// Slice the slice
fmt.Println(nums[1:3]) // Output: [4 5]
fmt.Println(nums[:2]) // Output: [3 4]
fmt.Println(nums[2:]) // Output: [5 6]

// Append: may reallocate underlying array
nums = append(nums, 7, 8)
fmt.Println(nums) // Output: [3 4 5 6 7 8]

slice 기능은 꽤 미니멀하며, Python에서 온 필자가 아쉬워했던 것 중 하나는 리스트 컴프리헨션이다. 리스트에서 몇 가지를 걸러내는 데 왜 for 루프와 if 문이 필요한가? 한 번 Go 팀원에게 왜 그런 기능이 없는지 물어본 적이 있는데, Go는 “시스템 언어”이므로 메모리 할당을 직접 제어할 수 있게 하려는 의도라고 답했다. 예를 들어 효율성을 위해 make()로 slice의 backing array를 미리 할당할 수 있다.

Map

Go의 map은 키를 값에 매핑하는 순서 없는 해시 테이블이다. slice와 마찬가지로 제네릭이며 타입 안전하므로, 예를 들어 map[string]int처럼 “문자열 키를 정수 값에 매핑하는 map”을 가질 수 있다.

map 자료형은 조회, 설정, 삭제, 존재 여부 검사, 순회를 제공한다. slice와 마찬가지로 “크기 힌트(size hint)”와 함께 make()를 사용해 메모리 할당을 제어할 수 있다.

map에 대해 할 말이 훨씬 더 많고 더 자세히 설명된 내용이나 구현에 대해 읽어볼 수도 있지만, 코드로 맛보기 예제를 보여주면 다음과 같다:

phrase := "the foo foo bar the foo"

counts := make(map[string]int)
for _, word := range strings.Fields(phrase) {
    counts[word]++
}

fmt.Println(counts)
// Output: map[foo:3 bar:1 the:2]

// map literal
maths := map[string]float64{
    "pi":  3.14,
    "tau": 6.28,
}

포인터, 하지만 안전하게

Go에는 포인터가 있지만 C나 C++와 달리 안전하다. 존재하지 않는 메모리를 가리킬 수 없으며, 런타임이 nil 포인터 역참조를 막아준다. 사실 포인터 연산 자체가 전혀 없으며, 무언가를 인덱싱하려면 안전한 slice를 사용하거나 저수준의 unsafe 패키지로 돌아가야 한다(필자는 한 번도 필요했던 적이 없다).

포인터는 C와 마찬가지로 *& 문법을 사용한다. *는 포인터 주소에 있는 값을 가져오고, &는 변수의 주소를 가져온다.

문법적으로 좋은 점 중 하나는 C 스타일의 -> 연산자가 없다는 것이다. 구조체 포인터를 역참조해 필드를 가져올 때도 .을 그대로 사용한다. 다음은 몇 가지 예제이다:

p := new(Person) // p is a "pointer to Person"
p.Name = "Joe Bloggs"
p.Age = 42
pers := *p // dereference p back to Person

// More succinct alternatives
p = &Person{"Joe Bloggs", 42}
p = &Person{Name: "Joe Bloggs", Age: 42}
pers = Person{"Joe Bloggs", 42}
p = &pers

Defer

Go에는 현재 함수가 반환되기 직전(또는 런타임 “panic”으로 종료되기 직전)에 주어진 함수 호출을 실행하는 고유한 키워드인 defer가 있다. defer가 여러 번 호출되면 함수들은 마지막에 defer된 것부터 먼저 호출된다. 이는 C++의 RAII나 Python의 with 문 같은 것을 대신해 리소스 정리에 사용된다.

필자가 아는 한 defer는 Go만의 제어 흐름 문이며, 명시적인 오류 처리 방식과 잘 어울린다. 더 자세히 읽어볼 수 있지만, 여기서는 흔한 작업인 파일 열고 닫기의 아주 간단한 예를 보여준다:

f, err := os.Open("file")
if err != nil {
    log.Fatal(err)
}
defer f.Close()
// read from f

고루틴(Goroutines)

고루틴(Goroutine)은 Go의 동시성 메커니즘으로, 스레드와 비슷하지만 훨씬 가볍다. 한 번에 10만 개, 심지어 100만 개의 고루틴을 동시에 살려두는 것도 어렵지 않다. Go 런타임이 고루틴을 스케줄링하며, 필요할 때(예: I/O가 준비되었을 때) 깨워 운영체제 스레드에서 실행한다.

Go 동시성 모델의 멋진 점 중 하나는 모든 표준 라이브러리 함수가 단순하고 동기적인 API를 가지고 있다는 것이다. 동시성이 필요하면 고루틴을 명시적으로 사용하면 된다. 이를 통해 일부 언어에서 볼 수 있는 “컬러 함수(colored functions)” 문제 — 비동기용과 동기용 두 벌의 API가 존재하는 문제 — 를 피할 수 있다.

고루틴을 시작하려면 go backgroundFunc()라고 쓰기만 하면 되고, Go 런타임이 backgroundFunc를 새로운 고루틴에서 실행한다. 다음은 사용자 가입을 기록한 뒤 백그라운드에서 이메일을 보내는 핸들러 함수의 간단한 예제이다(필자가 사이드 프로젝트에서 실제로 사용하는 코드와 유사하다):

func ProcessSignup(u *User) {
    u.SignedUpAt = time.Now()
    u.Save(db)
    go SendEmail(u.email, "Thanks for signing up!", "signup.html")
}

채널(Channels)

고루틴을 시작해도 promise나 고루틴 ID가 반환되지 않는다. 고루틴 간에 통신하거나 작업 완료를 알리고 싶다면 명시적으로 채널(channel)을 사용해야 한다. 채널은 Go의 주요 고루틴 간 통신 메커니즘이며, Go 격언에서 말하듯 “메모리를 공유함으로써 통신하지 말고, 통신함으로써 메모리를 공유하라.”

채널은 데이터를 주고받을 수 있는 타입 안전하고 스레드 안전한 큐이며, 동시에 동기화 역할도 한다. 채널에서 읽는 고루틴은 다른 고루틴이 채널에 쓸 때까지 대기한다.

다음은 단순한 “배열 합계” 작업을 병렬화한 예제이다. 실제로는 이 예제가 고루틴으로 이득을 볼 가능성은 거의 없지만, 개념을 이해하는 데 도움이 된다:

func main() {
    s := []int{7, 2, 8, -9, 4, 0}

    c := make(chan int)
    go sum(s[:len(s)/2], c) // first half
    go sum(s[len(s)/2:], c) // second half

    // Receive both results from channel
    x, y := <-c, <-c

    fmt.Println(x, y, x+y)
}

func sum(s []int, c chan int) {
    sum := 0
    for _, v := range s {
        sum += v
    }
    c <- sum // Send sum back to main
}

채널은 강력한 구조이며 할 말이 많다(버퍼드 vs 언버퍼드, 닫힌 채널 등). 하지만 그 부분은 Effective Go에 맡기겠다.

타입과 메서드

Go는 사용자 정의 타입을 지원하고 타입에 메서드를 붙일 수 있지만, 클래스는 없다(Go는 “classy”한 언어가 아니라고 말하는 사람도 있다). 그리고 struct와 인터페이스(아래에서 다룬다)는 있지만 상속은 없다. 모든 객체 지향적 장점은 컴포지션으로 구현되며, 임베딩(embedding) 같은 도구가 또 다른 접근 방식을 제공한다.

타입에 정의된 메서드는 “리시버(receiver)” 인자를 받는데, 이는 Python의 self나 다른 언어의 this와 유사하다. 하지만 몇 가지 독특한 특성(예: 리시버가 포인터일 수도 있고 값일 수도 있다)이 있다. 리시버 이름은 원하는 대로 지을 수 있지만, 보통 해당 타입의 첫 글자로 짓는다.

다음은 두 개의 필드를 가진 단순한 구조체와 String 메서드 예제이다:

type Person struct {
    Name string
    Age  int
}

func (p *Person) String() string {
    return fmt.Sprintf("%s (%d years)", p.Name, p.Age)
}

// Output: Bob (42 years)
func main() {
    p := &Person{"Bob", 42}
    fmt.Println(p.String()) // but .String() is optional; see below
}

인터페이스

인터페이스는 class MyThing implements ThatInterface처럼 명시적으로 선언해야 하는 Java 같은 다른 언어와는 조금 다르다. Go에서는 타입에 인터페이스의 모든 메서드를 정의하면 해당 타입이 암시적으로 그 인터페이스를 구현하게 되며, implements 키워드 없이도 인터페이스가 요구되는 곳 어디에서나 사용할 수 있다.

Go의 접근 방식은 종종 “정적 덕 타이핑(static duck typing)”이라고 불리며, 구조적 타이핑(structural typing)의 한 형태이다(TypeScript도 구조적 타이핑을 사용하는 인기 언어 중 하나다).

인터페이스는 표준 라이브러리와 Go 코드 전반에서 널리 사용된다. 가장 흔한 두 가지 예는 Printf 등이 값의 문자열 버전을 생성할 수 있게 하는 Stringer 인터페이스와, 파일, HTTP 서버, gzip 파일, 문자열 버퍼 등을 reader나 writer 스트림으로 다룰 수 있게 하는 io.Readerio.Writer 인터페이스이다.

다음은 StringerWriter 인터페이스의 정의이다. 둘 다 매우 단순한 단일 메서드 인터페이스로(작은 인터페이스는 Go에서 매우 흔하다), 실제로 직접 정의할 필요는 없지만 이 코드가 문법을 보여준다:

// Defined in package "fmt"
type Stringer interface {
    String() string
}

// Defined in package "io"
type Writer interface {
    Write(p []byte) (n int, err error)
}

// ...

func main() {
    p := &Person{"Bob", 42}
    fmt.Println(p.String())
    // Equivalent (Person implements Stringer, which Println looks for)
    fmt.Println(p)
}

Go에서 인터페이스의 중요성은 아무리 강조해도 지나치지 않다. 알고리즘을 제네릭하게 만들고 함수를 테스트 가능하게 하는 데 사용된다. Effective Go에서 더 자세히 읽어보라.

HTTP 서버 예제

마무리하기 전에 Go로 HTTP 서버를 작성하는 것이 얼마나 쉬운지 보여주는 작은 프로그램 두 가지를 소개한다. 그리고 이들은 단순한 장난감이 아니다. Go의 net/http 패키지는 프로덕션에서 바로 쓸 수 있다(다른 많은 언어에 내장된 웹 서버가 항상 “프로덕션에서는 사용하지 마세요”라고 적어 두어야 하는 것과는 다르다).

다음은 user 쿼리 스트링 파라미터를 그대로 에코하는 단일 라우트를 가진 아주 기본적인 HTTP 서버이다. http.ResponseWriterfmt.Fprintf에 전달되는 io.Writer로 사용하는 점에 주목하자:

package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/", handler)
    fmt.Println("listening on port 8080")
    http.ListenAndServe(":8080", nil)
}

func handler(w http.ResponseWriter, r *http.Request) {
    user := r.URL.Query().Get("user")
    if user == "" {
        user = "world"
    }
    fmt.Fprintf(w, "Hello, %s", user)
}

조금 더 발전된 예제로, 여기서는 몇 줄의 코드로 커스텀 정규식 기반 라우터를 갖춘 HTTP 서버를 만든다.

업데이트: 이런 종류의 커스텀 라우팅 코드는 더 이상 필요하지 않다. Go 1.22에서는 http.ServeMux가 개선되어 /user/{userId} 같은 경로를 직접 매칭할 수 있게 되었다.

// NOTE: use the new http.ServeMux routing in Go 1.22!

package main

import (
    "fmt"
    "net/http"
    "regexp"
)

type route struct {
    pattern *regexp.Regexp
    handler func(w http.ResponseWriter, r *http.Request, matches []string)
}

func home(w http.ResponseWriter, r *http.Request, matches []string) {
    fmt.Fprintf(w, "Home")
}

func user(w http.ResponseWriter, r *http.Request, matches []string) {
    user := matches[1]
    fmt.Fprintf(w, "User ID: %s", user)
}

func main() {
    routes := []route{
        {regexp.MustCompile(`^/$`), home},
        {regexp.MustCompile(`^/user/(\w+)$`), user},
    }
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        for _, route := range routes {
            matches := route.pattern.FindStringSubmatch(r.URL.Path)
            if len(matches) >= 1 {
                route.handler(w, r, matches)
                return
            }
        }
        http.NotFound(w, r)
    })
    fmt.Println("listening on port 8080")
    http.ListenAndServe(":8080", nil)
}

go 도구

최근 누군가 필자에게 가장 좋아하는 개발 도구가 무엇인지 물었다. 처음에는 “아마 Sublime Text?”라고 답했지만, 곧 생각을 바꿨다. (현재) 가장 좋아하는 개발 도구는 go 명령어라고 생각한다. Makefile 없이도 다음 모든 작업을 빠르게 수행할 수 있다:

go build          # build your project, produce a static executable
go run            # quick way to build and run, for development
go fmt            # format your .go files in the standard way
go test           # find and run your tests
go test -bench=.  # run all your benchmarks too

go mod init                # initialize a "Go modules" project
go get github.com/foo/bar  # fetch and install the "bar" package

그리고 이 외에도 훨씬 더 많은 명령어가 있다. 전체 문서를 읽어보라.

하지만 필자에게 가장 놀라운 점은 두 환경 변수인 GOOSGOARCH를 설정한 뒤 go build를 실행하면, Go가 주어진 운영체제와 아키텍처용으로 프로젝트를 크로스 컴파일한다는 것이다. macOS나 Windows 머신에서 배포 가능한 Linux 바이너리를 만드는 원라이너는 다음과 같다:

GOOS=linux GOARCH=amd64 go build

멋지지 않은가? Turbo Pascal 이후로 개발이 이렇게 쉬웠던 적이 있었던가…

마무리

Go와 그 생태계에 대해 할 이야기는 훨씬 더 많지만, 다른 언어에서 온 분들에게는 이 글이 도움이 되는 입문이 되었기를 바란다. 시작하려면 공식 Go Tour를 적극 추천한다. 더 깊이 들어가고 싶다면 Effective Go를 읽고, 이어서 훌륭한 책인 The Go Programming Language를 읽어보라.

그리고 Go로 작성하라!

이 글은 muse-spark-1.2-contributor 모델을 사용해 번역했습니다.

댓글