1P by GN⁺ 3시간전 | ★ favorite | 댓글 1개
  • Go는 백엔드 개발의 과도한 복잡성을 줄이는 선택지이며, 빠른 컴파일단일 바이너리 배포, 안정적인 의존성 관리가 핵심 장점임
  • Go는 데코레이터, 메타클래스, 매크로, trait, monad 같은 복잡한 추상화 대신 struct, 함수, 인터페이스, goroutine, channel 중심의 단순한 언어 설계를 택함
  • embed, html/template, net/http, database/sql, encoding/json, go test, pprof표준 라이브러리와 기본 도구만으로 웹 앱, 데이터베이스, 테스트, 벤치마크, 프로파일링까지 처리 가능함
  • goroutine은 약 2KB 비용의 stackful 실행 단위이며, channel, sync.Mutex, race detector, context.Context를 통해 동시성 처리와 취소 전파를 단순하게 다룰 수 있음
  • go mod init, go build, scp, systemctl restart로 이어지는 흐름은 node_modules, 복잡한 Docker·Kubernetes 구성, 과도한 마이크로서비스보다 하나의 Go 바이너리와 Postgres 중심의 단순 배포를 권함

Go를 선택해야 하는 이유

  • Go는 백엔드 개발의 과도한 복잡성을 줄이는 선택지이며, 빠른 컴파일, 단일 바이너리 배포, 안정적인 의존성 관리가 핵심 장점임
  • 프론트엔드에서 HTML이 과도한 복잡화의 대안으로 남아 있었듯, Go도 10년 넘게 백엔드 단순화를 위한 선택지로 존재해 왔음
  • 단순한 폼 제공이나 초당 약 40개 요청 수준의 CRUD 앱에 Node 패키지 다수, TypeScript 빌드 도구, Kubernetes, Rails 플랫폼 팀, Rust 재작성까지 동원하는 것은 과함
  • Go의 지향점은 “영리한 추상화”보다 읽기 쉬운 코드, 배포 가능한 결과물, 작은 운영 부담에 있음

지루함을 의도한 언어 설계

  • Go가 지루하게 느껴지는 이유는 의도적인 설계에 있으며, 데코레이터, 메타클래스, 매크로, trait, monad 같은 복잡한 추상화를 제공하지 않음
  • 핵심 구성 요소는 struct, 함수, 인터페이스, goroutine, channel 정도로 제한됨
  • 사양을 짧은 시간 안에 읽고 같은 날 생산적으로 코드를 작성할 수 있을 정도의 단순함을 목표로 함
  • 지루함은 팀 코드베이스에서 장점으로 작동함
    • 지난달 입사한 주니어도 2년 전 principal이 작성한 코드를 읽을 수 있음
    • gofmt가 하나의 포맷을 강제하므로 코드 스타일 논쟁이 줄어듦
    • 언어 자체가 지나치게 복잡한 추상화를 코드베이스에 끼워 넣기 어렵게 만듦

표준 라이브러리가 프레임워크 역할을 함

  • Go는 별도 웹 프레임워크 없이도 표준 라이브러리만으로 웹 앱을 만들 수 있음
  • embed, html/template, net/http를 사용하면 HTML 템플릿을 바이너리에 포함하고 HTTP 핸들러로 렌더링하는 앱을 구성할 수 있음
package main

import (
    "embed"
    "html/template"
    "net/http"
)

//go:embed templates/*.html
var files embed.FS

var tmpl = template.Must(template.ParseFS(files, "templates/*.html"))

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        tmpl.ExecuteTemplate(w, "index.html", map[string]string{
            "Name": "asshole",
        })
    })

    http.ListenAndServe(":8080", nil)
}
  • 이 예시는 동작하는 웹 앱이며, HTML 템플릿이 바이너리에 컴파일되어 포함됨
  • webpack, Vite, 개발 서버, 거대한 node_modules 없이 go build 후 파일 하나를 배포할 수 있음
  • 표준 라이브러리와 기본 도구만으로 주요 백엔드 작업을 처리할 수 있음
    • 데이터베이스: database/sql
    • JSON: encoding/json
    • 다른 서비스 호출: net/http 클라이언트
    • 동시 실행: go 키워드
    • 테스트: go test
    • 벤치마크: go test -bench
    • 프로파일링: pprof

깊이 있는 표준 라이브러리 구성

  • io.Readerio.Writer

    • io.Readerio.Writer는 각각 메서드 하나만 가진 인터페이스지만, Go 생태계 전반의 중요한 기반으로 작동함
    • HTTP 응답 본문을 gzip writer로 연결하고 다시 디스크 파일로 이어 붙이는 식의 조합을 적은 코드로 처리할 수 있음
    • 주요 패키지들이 이 두 인터페이스를 공유하므로, 같은 패턴을 여러 곳에서 반복적으로 활용할 수 있음
  • context.Context

    • context.Context는 취소 전파를 위한 표준 방식임
    • 사용자가 브라우저 탭을 닫으면 요청 context가 취소되고, 이어서 데이터베이스 쿼리와 하위 HTTP 호출까지 취소될 수 있음
    • goroutine 누수나 연결 풀을 소모하는 좀비 쿼리를 피하려면 context를 첫 번째 인자로 전달하고 존중해야 함
  • 인코딩 패키지

    • encoding/json, encoding/xml, encoding/csv, encoding/binary가 모두 표준 라이브러리에 포함됨
    • struct tag 패턴과 포인터로 디코딩하는 사용감이 유사하므로 하나를 익히면 다른 패키지도 쉽게 사용할 수 있음

고통을 줄이는 동시성 모델

  • goroutine은 OS thread 자체가 아니며, 런타임이 OS thread 위에 다중화하는 stackful 실행 단위임
  • goroutine은 시작 비용이 약 2KB이며, 노트북에서도 10만 개를 생성할 수 있음
  • channel은 goroutine 사이의 타입 있는 파이프로 동작하며, 한쪽에서 보내고 다른 쪽에서 받으면 런타임이 동기화를 처리함
  • 공유 상태가 필요할 때는 sync.Mutex를 사용할 수 있고, race detector가 데이터 레이스를 찾아줌
  • 병렬 HTTP fetcher도 별도 라이브러리나 프레임워크, async/await 의식 없이 작성 가능함
results := make(chan string, len(urls))
for _, url := range urls {
    go func(u string) {
        resp, _ := http.Get(u)
        results <- resp.Status
    }(url)
}
for range urls {
    fmt.Println(<-results)
}

실제 CRUD 라우트 예시

  • Postgres에서 게시글을 읽고 HTML을 렌더링하는 CRUD 성격의 라우트도 한 화면에 들어갈 정도로 단순하게 구성됨
//go:embed templates/*.html
var tmplFS embed.FS

var tmpl = template.Must(template.ParseFS(tmplFS, "templates/*.html"))

type Post struct {
    ID    int
    Title string
    Body  string
}

func postsHandler(db *sql.DB) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        rows, err := db.QueryContext(r.Context(),
            "SELECT id, title, body FROM posts ORDER BY id DESC LIMIT 50")
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
        defer rows.Close()

        var posts []Post
        for rows.Next() {
            var p Post
            if err := rows.Scan(&p.ID, &p.Title, &p.Body); err != nil {
                http.Error(w, err.Error(), http.StatusInternalServerError)
                return
            }
            posts = append(posts, p)
        }

        tmpl.ExecuteTemplate(w, "posts.html", posts)
    }
}
  • 이 예시는 데이터베이스, 템플릿, HTTP 핸들러를 한곳에서 보여줌
  • r.Context()가 SQL 쿼리에 전달되므로 연결이 닫히면 쿼리도 취소될 수 있음
  • ORM, DI 컨테이너, 서비스 계층, 추상 기반 클래스가 많은 controllers/ 디렉터리 없이 위에서 아래로 읽으며 동작을 파악할 수 있음

주말을 망치지 않는 의존성 관리

  • go mod init으로 모듈을 시작하면 의존성은 go.modgo.sum에 기록됨
  • go.sum은 실제로 받은 항목에 대한 암호학적 기록으로, 기대한 것과 다른 의존성이 들어오는 상황을 확인할 수 있게 함
  • node_modules 디렉터리, 개발 환경과 CI 사이의 lockfile drift, peer dependencies, optional dependencies, devDependencies, peerDependenciesMeta 같은 복잡성이 없음
  • 오프라인 빌드가 필요하면 go mod vendor가 의존성을 vendor/ 디렉터리에 내려받고, 툴체인이 이를 자동으로 사용함
  • 프로젝트 전체와 의존성을 tarball 하나에 담을 수 있어 운영과 보안 검토 측면에서 유리함

컴파일러와 함께 제공되는 도구

  • Go의 기본 도구들은 서드파티 플러그인이나 별도 설정 파일 없이 제공됨
  • gofmt는 코드 포맷을 표준화하며, 포맷 논쟁과 공백 변경으로 인한 diff 증가를 줄임
  • go vet은 명백한 실수를 잡는 데 사용됨
  • go test는 테스트를 실행함
  • go test -race는 race detector와 함께 테스트를 실행해 데이터 레이스를 찾음
  • go test -bench는 벤치마크를 실행함
  • go test -cover는 테스트 커버리지를 확인함
  • go tool pprof는 실행 중인 프로덕션 서비스의 HTTP 엔드포인트를 통해 CPU와 메모리 사용량 flame graph를 얻을 수 있게 함

배포는 복사 명령으로 끝남

  • Go 배포의 핵심 흐름은 바이너리를 빌드하고 서버에 복사한 뒤 실행하는 것임
GOOS=linux GOARCH=amd64 go build -o myapp ./cmd/myapp
scp myapp user@server:/usr/local/bin/
ssh user@server 'systemctl restart myapp'
  • 이 흐름은 Dockerfile, multi-stage build, base image CVE 알림, Kubernetes manifest, Helm chart, ArgoCD, service mesh, sidecar 없이 배포 가능함
  • 12MB의 정적 링크 바이너리와 20줄짜리 systemd unit 파일만으로 프로덕션 배포가 가능함
  • Docker가 꼭 필요하다면 Go 바이너리를 FROM scratch 이미지에 넣는 방식으로 충분함

프레임워크와의 대비

  • Rails, Django, Express, Next.js 같은 프레임워크에는 각자의 배포 절차, ORM, admin, middleware, npm 경고, 라우팅 관례 변화 같은 부담이 있음
  • Go 바이너리는 컴파일되어 실행되며, 5년 뒤에도 실행될 수 있는 안정성을 장점으로 가짐
  • 프레임워크가 더 빨리 폐기되거나 유지보수자가 번아웃을 호소할 수 있다는 대비 속에서, Go의 단순한 실행 모델이 두드러짐

마이크로서비스보다 단일 Go 바이너리

  • 마이크로서비스가 기본 선택지가 되어서는 안 되며, 먼저 모놀리스를 작성하는 편이 좋음
  • 권장 구성은 하나의 Go 바이너리, 하나의 Postgres, 꼭 필요할 때만 하나의 Redis임
  • HTML과 JSON API를 같은 포트에서 제공하고, 단일 VPS에서 실행하는 구성이 가능함
  • Go는 goroutine 비용이 낮고 동시성 처리가 강하므로, 초당 1만 요청까지도 무리 없이 확장할 수 있음
  • 실제로 분리할 필요가 생기면 Go 모놀리스에서 패키지를 별도 저장소로 옮기는 방식으로 나눌 수 있음
  • 인터페이스가 이미 존재하므로, 언어가 자연스럽게 분리를 고려한 구조를 만들게 함

제네릭과 에러 처리

  • if err != nil은 버그가 아니라 기능임
  • 각 실패 지점에서 무엇을 할지 직접 판단하게 만들어 에러를 숨기지 않음
  • try/catch 중첩은 에러를 없애는 것이 아니라 프로덕션 장애 시점까지 숨길 수 있음
  • 제네릭은 Go 1.18에 도입되었고, 필요할 때 사용하면 됨

결론

  • 프레임워크, 마이크로서비스, Rust 재작성, 새 JavaScript 메타프레임워크가 꼭 필요한 것은 아님
  • go mod init을 실행하고, main.go를 작성하고, 템플릿을 embed한 뒤 컴파일해 배포하는 단순한 흐름을 권함
  • 지루한 선택이 올바른 선택이며, Go가 그 선택지임
Lobste.rs 의견들
  • 전달자를 탓하려는 건 아니지만, 이런 블로그 문체는 피곤하고 유치함. 처음엔 웃겼을지 몰라도 반복될수록 짜증이 기하급수적으로 커짐
    그래도 Go는 좋음. 최근 TypeScript 프로젝트에서 Go 프로젝트로 옮겼는데 정신 건강과 업무 사기가 빠르게 좋아지고 있음
    if err != nil이 버그가 아니라 기능이라는 말은 받아들이지만, 여전히 Go의 가장 큰 흠이라고 봄. 합 타입(sum types) 이 있었다면 실행 시간 타입 단언에 의존하지 않고도 훨씬 인체공학적으로 만들 수 있었을 것임

    • 모든 것에 양다리 걸치고 실제 입장은 없는 AI 잡글보다는 이런 글이 낫다고 봄
    • 재미있게 읽었지만 이런 류의 글을 많이 보진 않았음. 그래도 “dipshit”보다는 “walnut”이라고 부르는 쪽이 더 웃겨서 좋음
      이런 식으로 쓸 거면 최소한 모욕도 좀 재치 있게 했으면 함
    • 동의함. 신고할 방법이 있나? 어떤 신고 분류에도 맞지 않음
  • 다른 댓글을 보면 인기 없는 생각 같고 거칠게 들리긴 싫지만, Go가 정말 싫음
    Go는 동시성에 효율적인 런타임 위에 그럭저럭 나쁘지 않은 문법을 얹고, Google의 힘으로 생태계를 밀어붙인 언어임. 그 외에는 끔찍하다고 봄
    가장 큰 문제는 수십 년간의 프로그래밍 언어 설계 연구나 심지어 실무 관행까지 의도적으로 무시하려고 설계된 것처럼 보인다는 점임. 수십 년 뒤에야 제네릭이 생기긴 했지만
    항상 의존 타입을 써야 한다는 뜻은 아니지만, 그래도 정도가 있음. Go에는 현대 언어라면 가져야 할 데이터 모델링, 불변 조건 모델링, 코드 구조화 기능이 거의 없음. Rust는 학습 곡선이 더 가파르지만 이런 면에서는 훨씬 낫고, 꼭 Rust만큼 정교한 타입 시스템이 아니어도 충분히 괜찮을 수 있음. 컴파일 시간이 걱정이라면 단순하지만 쓸 만한 기능만으로도 빠르고 표현력 있는 건전한 타입 시스템을 만들 수 있음
    그리고 if err != nil은 오류 처리 잡음으로 코드를 도배하는 최악의 방식이라고 봄. Go 쪽은 합 타입에 왜 그렇게 반감이 있는지 모르겠음. 이 점에서는 Java 예외조차 더 낫다. 현실은 언어에 오류를 더 잘 다룰 기능이 없으니, 사람들이 가능한 최악의 땜질을 기능으로 착각하게 된 것임
    애초에 원문이 잘난 체하지 않았다면 이런 댓글도 안 썼을 것임. “그냥 X 써라”는 멍청한 말임. 사용 사례에 맞고 편하고 생산적인 도구를 쓰면 됨. 그게 Go라면 Go를 쓰고, 아니면 다른 걸 고르면 됨

    • Go가 설계 공간에서 차지하는 위치는 대규모 코드베이스와 대규모 조직에서 일하는 신입 개발자들의 단순성을 거의 모든 것보다 우선하는 선택이라고 봄. 그래서 경험이 적은 개발자도 많은 맥락을 쌓지 않고 코드를 읽고 국소적으로 고치기 쉬운 편임
      특히 Google 같은 조직에서는 수천 명의 개발자가 있고, 특정 팀이나 회사에 머무는 기간이 짧을 수 있으니 도움이 됨
      이런 맥락에서는, 특히 미숙한 개발자에게 고급 타입 시스템의 부재가 어느 정도 장점이 되기도 함. 기본 타입이나 구조체 같은 아주 기본 개념을 넘어서 타입을 거의 생각하지 않아도 되기 때문임. 데이터를 모델링하는 도구는 거의 주지 않지만, 반대로 별생각 없이 많은 코드를 쓸 수 있음
      언어 수준의 정확성에는 별로 좋지 않다고 봄. 하지만 대규모 조직에서는 모노레포 분석, CI/CD, 카나리아 테스트, 관측 도구 같은 주변 인프라에 더 많이 의존하게 됨. 작은 조직보다 그 인프라가 훨씬 더 하중을 떠받침
      나도 비슷한 낮은 인지 부하 때문에 Go를 어느 정도 좋아함. 특정 프로젝트에 가끔만 코드를 쓰고, 현재 매일 장기 프로젝트에 깊게 관여하지 않기 때문임. 한 달 동안 안 본 코드베이스에 들어가 한 시간 미만으로 작업할 수 있다는 건 큰 장점임. 다만 복잡한 프로젝트의 전업 개발자였다면 덜 좋아했을 것 같음
    • Dart도 수십 년 연구를 무시하는 것처럼 보이지 않는 Google 언어지만, Flutter 밖에서는 아무도 안 씀. Go는 괜찮음
    • 이 글은 공격적이고 잘난 체하는 밈 형식을 베낀 것임. 그래서 사람들을 자극할 게 뻔하고, 글의 요지는 불꽃싸움이 아니라 제대로 된 대화를 할 가치가 있다고 봐서 별로임
      Go 개발자들은 기본기를 제대로 잡는 데 집중했다고 봄. 지금까지의 언어들과 프로그래밍 언어 이론 연구 커뮤니티가 기본기를 무시해왔기 때문임. 사람들은 가장 포괄적인 타입 시스템에 집착하지만, 타입 시스템이 복잡하고 표현력 있어질수록 수익은 줄어들고, 타입 시스템에 아무리 힘을 쏟아도 끔찍한 패키지 관리, 팀이 새 DSL을 배워야 하는 빌드 도구, 타입 정보나 서드파티 패키지 문서 링크를 자동 생성하지 않는 문서 시스템, 빈약한 표준 라이브러리, 심각한 성능 문제, 정적 컴파일 전략 부재, 고통스러운 빌드 시간, 가파른 학습 곡선, 징벌적인 타입 시스템, 읽기 어려운 문법, 형편없는 편집기 통합을 보상할 수는 없음
      Go에 데이터 모델링 기능이 전혀 없다는 말은 명백히 틀림. 어떤 언어에서도 데이터와 불변 조건은 모델링할 수 있고, Go도 그 모델을 강제할 타입 시스템을 꽤 제공함
      Rust는 훌륭하고, 반복 속도가 중요하지 않거나 베어메탈에 배포하거나 정확성·성능 요구가 아주 강하면 좋은 선택임. 하지만 범용 애플리케이션 개발, 특히 팀 단위 개발의 기본값으로는 좋지 않음. if err != nil을 많이 치긴 하지만, 초당 키 입력 수가 병목인 사람은 없다고 봄
    • Rust 말고는 이런 기능을 가진 현대 언어가 거의 없음. Gleam이나 Swift로 프로그래밍하고 싶다면 몰라도, 그 정도로 틈새라면 차라리 Haskell을 쓰는 셈임
  • if err != nil이 버그가 아니라 기능이고, 문제가 생길 수 있는 모든 지점을 보게 만든다는 말은 틀림
    실제로는 강제하지 않음. 직접 확인하지 않으면 오류를 무시하는 게 더 쉬움
    오류를 처리하거나 전파하는 방식에서는 Rust가 여전히 빛나는 사례임

    • 맞음. errcheck 같은 게 없으면 오류는 너무 쉽게 무시되며, 그건 그냥 어리석음. 적어도 오류를 명시적으로 버리도록 강제해야 함
      다행히 최근 몇 년간 작업한 모든 Go 프로젝트는 Go의 빈약한 내장 정적 검사 위에 golangci-lint를 얹어 썼음. 솔직히 모든 Go 프로젝트에 필수여야 함
    • 이 부분은 Swift가 더 좋음. 기능적으로는 같은 모델이지만, Swift 쪽은 서로 다른 라이브러리의 오류 전파가 더 쉬워짐. 다만 장단점과 설계 선택이 다른 것일 뿐, 더 좋거나 나쁘다고 할 문제는 아님
  • 이런 글쓰기 유행은 정말 싫지만, 글이 말하려는 요지에는 동의함
    “Volkswagen 크기의 node_modules가 없다”는 말은 맞지만, 프로젝트 로컬 node_modules가 아니라 ~/go에 있는 전역 패키지 캐시일 뿐임

    • 게다가 홈 디렉터리를 더럽힘. 앞에 점조차 없음. 이게 어떻게 용인되는지 모르겠음
    • 다른 언어 생태계의 의존성 트리 크기를 욕하기 전에 wc -l go.sum부터 해보라고 늘 말하고 싶음
  • 페이지 열자마자 “Hey, dipshit.”이 보여서 바로 닫음

  • 프로그래밍 언어를 추켜세우는 글 대부분과 같은 문제를 가짐. 현재 언어가 얼마나 훌륭한지보다, 이전에 쓰던 언어가 얼마나 끔찍했는지에 더 집중함
    필자는 Ruby와 TypeScript, 어쩌면 Python에서 심각한 고통을 겪었고 Go가 그걸 해결해준 듯함. 하지만 나는 Ruby나 TypeScript를 쓰지 않아서 글이 별로 와닿지 않았음
    수년간 이런 변주를 수십 번 읽은 느낌임. Python과 JavaScript와 달리 정적 타입이 있으니 Haskell을 써라. Perl과 Erlang과 달리 단일 바이너리로 배포할 수 있으니 Rust를 써라. Ruby와 Tcl과 달리 제대로 된 동시성과 채널이 있으니 Elixir를 써라
    필자가 자신에게 맞는 언어를 찾은 건 기쁘지만, 그 조언을 따르지는 않을 것임

    • 여기에는 Go를 Lobsters 독자에게 팔아야 한다고 생각하는 사람이 꽤 있는 듯함. 어떤 사람들에게는 오히려 역효과가 날 수 있음
  • Go의 제로값은 늘 흠이라고 느꼈음. 사용자가 기본값을 명시하도록 강제하는 편이 더 낫다고 봄. 그 외에는 OCaml이 아니라는 점을 감안하면 꽤 좋은 언어임

    • 제로값을 좋아하고 꽤 영리하다고 생각함. 하지만 기본값 설정 기능은 정말 아쉬움. 예를 들어 누락된 booltrue 값을 가져야 하는 JSON 객체를 마샬링하기가 매우 어려움
  • 배포와 컴파일 경험은 훌륭하지만, 언어 자체를 작성하는 건 정말 싫음. 쓸 때마다 나쁜 경험이 됨. Go처럼 제약이 심하지 않으면서도 배포 경험이 좋은 다른 언어가 있나?
    내가 Go에서 뭔가 놓치고 있는 건가?
    최근 작은 Rails 애플리케이션을 배포해보니 설정이 너무 많이 필요해서 Go의 장점은 확실히 appreciate하게 됨

    • 최근 Rust 프로젝트를 x86_64-unknown-linux-musl로 컴파일하기 시작했음. 이렇게 하면 모든 64비트 Linux 머신에서 그냥 실행되는 정적 바이너리가 나옴. 그다음 scp로 옮겨서 실행함
      아직 포트를 할당하고 수동으로 시작해야 하는 문제는 남아 있지만, 약간의 systemd 마법으로 해결할 계획임
    • 배포 경험 측면에서는 회사에서 nix bundler로 의외로 큰 성공을 거뒀음. 맥락을 말하자면 Qt6 GUI 앱을 만들고 있음
      bundler를 쓰면 단일 실행 파일을 만들 수 있고, 다른 배포판의 Linux 머신에 떨어뜨려도, 심지어 Qt가 설치되어 있지 않아도 사용자가 실행 파일만 실행하면 전체 GUI가 동작함
      다만 OpenGL 드라이버에서는 문제가 있다는 단서가 있음. 여전히 가능하긴 하지만 “복사해서 실행”보다 복잡해짐
  • Go가 동시성을 위해 설계됐다고 주장하면서도, 실수로 쉽게 공유할 수 있는 원시 포인터가 내장되어 있다는 게 가장 큰 문제임

  • 지루함 자체는 괜찮지만, Go는 실제로 지루한 언어가 되는 데 유난히 실패한다고 봄
    “데코레이터가 없다”고 하지만 struct tags와 리플렉션은 있음. 실행해보기 전까지 이것들이 어떻게 상호작용하는지 파악하기 어려움
    구조적 인터페이스와 리플렉션은 멀리 떨어진 곳에서 행동이 바뀌는 무서운 원천임. 구조체에 잘못된 메서드 하나를 추가했을 뿐인데 라이브러리 동작이 완전히 바뀔 수 있음
    문서화 관점에서도 이상함. 타입이 어떤 인터페이스를 만족하도록 의도됐는지 명확히 드러내고 싶지 않을 이유가 있나?
    고루틴은 왜 그냥 스레드라고 부르지 않는지 모르겠음
    채널은 왜 언어 기능이어야 하나? 제네릭이 세 가지쯤 되는 타입 말고도 유용하다는 걸 인정하는 데 10년 늦었기 때문이라고 봄

    • 고루틴은 스레드가 아니라, 스레드 풀 위에서 동작하는 더 가벼운 추상화임. 그래서 수천 개의 고루틴을 쉽게 만들 수 있음
      채널이 런타임 일부인 건 고루틴 스케줄러가 채널을 알아야, 채널이 비어 있지 않게 됐을 때 받는 쪽 고루틴을 더 쉽게 깨울 수 있기 때문이라고 봄. 아마 이 방식이 더 쉬웠을 것임
    • 고루틴은 여러 부가 도구가 붙은 그린 스레드이기 때문임