1P by GN⁺ 7시간전 | ★ favorite | 댓글 1개
  • 세 언어의 철학과 가치관의 차이를 중심으로, 각 언어가 어떤 문제를 해결하려는지 비교
  • Go는 단순성과 안정성을 중시하며, 기능을 최소화해 협업과 유지보수를 쉽게 만드는 언어로 설명됨
  • Rust는 안전성과 성능을 동시에 추구하며, 복잡한 타입 시스템과 트레이트 구조로 메모리 안전성을 보장
  • Zig는 수동 메모리 관리와 데이터 중심 설계를 통해 개발자에게 완전한 제어권을 부여하는 실험적 언어로 묘사됨
  • 세 언어의 상반된 접근은 프로그래밍 언어가 구현하는 가치 체계를 드러내며, 개발자가 어떤 철학에 공감하느냐가 선택의 기준이 됨

언어 비교의 관점

  • 작성자는 직장에서 사용하는 언어가 아닌, 새로운 언어 실험을 통해 각 언어의 가치 체계를 이해하려 함
  • 단순히 기능 목록을 비교하기보다, 언어가 어떤 트레이드오프를 선택했는가가 중요하다고 강조
  • Go, Rust, Zig는 기능적으로 겹치는 부분이 많지만, 설계자가 중시한 가치가 다름
  • 각 언어의 철학을 파악함으로써, 어떤 환경과 목적에 적합한지 판단 가능

Go — 단순함과 협업 중심의 언어

  • Go는 미니멀리즘으로 구별되며, “머릿속에 전체 언어를 담을 수 있다”는 특징을 가짐
    • 제네릭은 12년 만에 추가되었고, 태그드 유니언이나 에러 처리 문법 설탕 같은 기능은 여전히 없음
  • 기능 추가에 매우 신중하여, 보일러플레이트 코드가 많지만 언어의 안정성과 가독성이 높음
  • Go의 슬라이스(slice) 는 Rust의 Vec<T>나 Zig의 ArrayList 기능을 포괄하며, 메모리 위치를 런타임이 자동 관리
  • C++의 복잡성과 컴파일 지연에 대한 불만에서 출발해, 단순하고 빠른 컴파일을 목표로 설계
  • 기업 환경에서의 협업 효율성을 중시하며, 복잡한 기능보다 명료한 코드와 일관성을 우선

Rust — 복잡하지만 강력한 안전성과 성능

  • Rust는 “제로 비용 추상화” 를 내세우며, 다양한 개념이 결합된 맥시멀리스트 언어
  • 학습 난이도가 높은 이유는 개념 밀도가 높기 때문이며, 복잡한 타입 시스템과 트레이트 구조가 존재
  • Rust의 핵심 목표는 성능과 메모리 안전성의 양립
    • UB(Undefined Behavior) 를 방지하기 위해 컴파일 타임에 검증 수행
    • 잘못된 포인터 참조나 이중 해제 등으로 인한 예측 불가능한 동작을 차단
  • 컴파일러가 코드의 런타임 동작을 이해할 수 있도록, 개발자는 명시적으로 타입과 트레이트를 정의해야 함
  • 이러한 구조 덕분에 타인의 코드에 대한 신뢰성이 높고, 라이브러리 생태계가 활발하게 유지됨

Zig — 완전한 제어와 데이터 중심 설계

  • Zig는 세 언어 중 가장 신생 언어로, 버전 0.14 단계이며 표준 라이브러리 문서화가 거의 없음
  • 수동 메모리 관리를 채택해, 개발자가 직접 alloc()을 호출하고 할당자(allocator) 를 선택해야 함
  • Rust나 Go와 달리, 전역 변수 생성이 간단하며, 런타임에서 “illegal behavior”를 감지해 프로그램을 중단
    • 빌드 시 선택 가능한 4가지 릴리스 모드로 성능과 안정성의 균형 조정 가능
  • 객체지향 프로그래밍(OOP) 기능을 의도적으로 배제
    • private 필드동적 디스패치가 없으며, std.mem.Allocator조차 인터페이스로 구현되지 않음
    • 대신 데이터 중심 설계(data-oriented design) 를 지향
  • 메모리 관리 또한 RAII 방식의 세밀한 객체 단위 관리 대신, 큰 메모리 블록을 주기적으로 할당·해제하는 구조를 권장
  • Zig는 자유롭고 반체제적인 성향의 언어로 묘사되며, OOP적 사고를 제거하고 개발자 주도 제어를 극대화
  • 현재 팀은 모든 의존성 재작성 작업에 집중 중이며, 안정 버전(1.0)은 아직 미정

결론 — 언어가 드러내는 가치의 차이

  • Go는 협업과 단순성, Rust는 안전성과 성능, Zig는 자유와 제어권을 중심 가치로 삼음
  • 세 언어의 차이는 단순한 기능 비교가 아니라, 소프트웨어 개발에 대한 철학적 선택을 반영
  • 개발자는 자신이 어떤 가치에 공감하는가에 따라 언어를 선택하게 됨
Hacker News 의견
  • Rust에서 mutable global variable을 만드는 건 어렵지 않음
    단지 unsafe나 동기화를 제공하는 스마트 포인터를 써야 함
    Rust는 기본적으로 re-entrant하며 컴파일 타임에 스레드 안전성을 보장하기 때문임
    만약 정적 스레드 안전성을 신경 쓰지 않는다면 Zig나 C처럼 쉽게 만들 수 있음
    차이는 Rust는 코드의 런타임 동작에 대해 더 많은 보증 도구를 제공한다는 점임

    • 여러 해 Rust를 써본 입장에서, mutable global variable은 “할 수 있다고 해서 해야 하는 건 아니다”의 전형적인 예라고 생각함
      다른 언어로 돌아가서 이런 걸 아무렇지 않게 쓰는 걸 보면 안전성 측면에서 미친 짓처럼 느껴짐
    • “trivial하다, 단지 ~가 필요하다”는 식의 표현은 C++이나 Perl, Haskell에서도 들었던 이야기임
      하지만 이런 식의 “단순한 일”들이 쌓이면 결코 단순하지 않게 됨
      Rust는 이미 그 선을 넘었고, 이제는 결코 trivial하지 않음
    • Rust 컴파일러가 스레드 간 race condition을 컴파일 타임에 잡아주는지 궁금함
      그렇다면 C보다 매력적일 것 같음
      두 변수가 항상 함께 잠겨야 하는 상황에서는 어떻게 처리하는지도 알고 싶음
    • 내가 언어를 만든다면 mutable global variable은 아예 금지시킬 것임
      디버깅하다 보면 결국 문제의 근원은 항상 그쪽이었음
  • Rust의 개념 밀도를 지적한 글에 대해, 실제로는 그 중 5%만 알아도 생산적으로 쓸 수 있다고 생각함
    12년 넘게 Rust를 써왔지만 #[fundamental] 같은 건 한 번도 쓸 일이 없었음
    Rust에서도 arena allocation을 할 수 있고, allocator 개념도 존재함
    기본 allocator가 있을 뿐이며, 보통 Box::new 같은 명시적 힙 할당을 사용함
    mutable global은 static FOO: Mutex<T> = Mutex::new(...)처럼 만들 수 있고, 메모리 안전성을 위해 mutex가 필요함
    Rust의 타입 시스템은 메모리 안전성뿐 아니라 코드의 의미적 안전성까지 보장하도록 설계되어 있음

    • 하지만 다른 개발자가 다른 5~10%의 개념을 쓸 수 있기 때문에, 협업 시 결국 더 많은 개념을 배워야 함
      C에서는 이런 복잡성이 적음
      복잡도는 결국 중요한 문제임
    • “Rust도 arena allocation이 가능하다”는 말은 맞지만, 대부분의 Rust/Go 코드는 작은 단위의 다수의 할당을 기본 경로로 가짐
      단순히 가능 여부의 문제가 아니라 기본적인 프로그래밍 스타일의 차이에 대한 이야기임
    • Rust에서 allocator가 타입이라면, m:n 스레드 모델에서 각 요청마다 별도의 arena를 줄 수 있는지도 궁금함
    • Rust의 allocator가 전역(global) 인지, 모든 힙 할당이 같은 allocator를 사용하는지도 질문함
    • Casey Muratori의 batch allocation 영상을 언급하며, 일부 개발자들이 이를 잘못 이해하고 Rust의 RAII를 비판한다고 지적함
      Zig Software Foundation이 Asahi Lina의 Rust 관련 발언을 잘못 인용한 사례도 있었음
      Zig가 다른 언어를 깎아내리는 마케팅 태도는 별로 마음에 들지 않음
  • Zig가 마음에 드는 이유는 메모리 고갈을 우아하게 처리할 수 있는 언어이기 때문임
    모든 할당이 실패 가능(fallible)하다고 가정하고, 명시적으로 처리해야 함
    스택 공간도 마법처럼 다루지 않고, 컴파일러가 호출 그래프를 분석해 최대 크기를 추론함
    임베디드 환경에서 이런 자원 중심 설계는 필수적임

    • 하지만 Linux처럼 overcommit을 사용하는 OS에서는 실제로 할당 실패가 발생하지 않음
      언어 차원의 처리로는 해결되지 않음
    • Zig가 존재해야 하는 이유가 Rust가 이미 있는데 무엇이냐는 질문에, 차라리 “C가 있는데 왜 Zig인가?”를 묻겠음
      결국 수동 메모리 관리라는 같은 문제를 안고 있음
      그렇다면 GC 언어를 쓰는 게 낫다고 생각함
    • 재귀나 함수 포인터 호출이 있을 때 Zig의 스택 크기 추론이 어떻게 작동하는지 궁금함
    • Zig가 처음은 아니며, 1958년 JOVIAL 이후의 시스템 언어 역사를 살펴볼 필요가 있음
    • Rust에서도 pre-allocation을 잘 처리할 수 있음
      다만 Rust의 표준 라이브러리는 OOM 시 panic을 사용하기 때문에, no-std 환경에서 임베디드 개발을 지원하는 생태계가 따로 있음
  • Go의 slice는 Rust의 Vec<T>와 다름
    append()는 새로운 slice를 반환하며, 기존 메모리를 공유할 수도 있고 아닐 수도 있음
    메모리를 줄이는 방법도 없고, append(s, ...)만 쓰면 새 slice를 무시하게 됨
    Go는 “내가 말한 대로 해라”는 태도이고, Rust는 “내가 말한 대로 했는지 검증하라”는 태도임
    즉, Go는 단순함을 위해 실수를 허용하고, Rust는 복잡해지더라도 실수를 줄이는 방향을 택함

    • 실제로는 slices.Clip으로 메모리를 줄일 수 있음
      또한 append(s, ...)만 쓰면 컴파일 오류가 나므로, 원문은 약간 부정확한 주장
      Go는 기능 추가 시 복잡도 증가를 신중히 따지는 언어임
    • “append(s, …)”는 컴파일조차 되지 않기 때문에 초보자가 그런 실수를 할 수 없다고 생각함
    • Go에 제네릭이 생겼는데도 List[T] 같은 타입이 널리 쓰이지 않는 게 흥미로움
      아마 growable list를 직접 넘길 일이 많지 않기 때문일 것임
    • Go는 명세와 문서에 대부분의 함정(foot gun) 이 명확히 적혀 있음
      단순히 문서를 안 읽고 놀라워하는 경우가 많음
  • C/C++의 UB(Undefined Behavior) 를 런타임 검사로 잡는 건 현실적으로 어렵다고 생각함
    Android도 sanitizer를 모든 커밋에 적용했지만, Rust로 전환하고 나서야 익스플로잇이 줄어듦

    • Android의 sanitizer 관련 주장에 대한 출처를 요청함
  • 언어 비교 글이 각 언어의 강점과 약점을 솔직하게 다뤄서 좋았음
    다만 Raku가 언급되지 않아 아쉬움
    내 생각엔 C–Zig–C++–Rust–Go가 저수준 언어의 연속선이라면, 고수준 쪽은 Julia–R–Python–Lua–JS–PHP–Raku–WL로 이어짐

    • WL이 무엇인지 질문함
    • Raku는 표현력이 풍부한 범용 언어로, 다중 디스패치·역할(roles)·점진적 타입·게으른 평가·강력한 정규식 시스템을 내장함
      문법 정의를 언어 차원에서 지원해 DSL이나 로그 파싱이 쉬움
      VM 기반이라 성능은 낮지만, 문제 구조를 직접 표현하기에 적합함
      Perl의 후계로서 유연하고 일관된 언어를 지향함
  • Rust에서 함수가 포인터를 반환하면 힙 할당이 자동으로 일어난다고 생각하는 건 오해임
    지역 변수는 스택에 있고, 반환 시 사라지므로 포인터는 무효화됨
    Rust는 안전 모드에서는 포인터 역참조가 불가능하고, unsafe 모드에서는 개발자가 유효성 보장 책임을 짐
    아마도 Box::new를 “암묵적 할당”으로 착각한 듯함

    • Go의 escape analysis와 Rust의 명시적 힙 할당을 혼동하는 건 이해하기 어려움
      이는 개념을 잘못 이해했거나 의도적으로 오도하는 것처럼 보임
  • Go의 가장 큰 장점은 간단한 동시성 모델
    goroutine 덕분에 쉽게 병렬 코드를 작성할 수 있음

    • Go의 장점 중 하나는 코드 일관성 덕분에 대규모 코드베이스를 탐색하기 쉽다는 점임
      인터페이스 구현을 찾는 건 어렵지만, 가독성이 높아 팀 협업에 유리함
    • Rob Pike의 “Concurrency is not Parallelism” 강연이 Go의 동시성 철학을 잘 설명함
      colored function이 없고, 채널 기반 통신이 단순해 정확한 동시성 코드를 빠르게 작성할 수 있음
    • 하지만 Structured Concurrency가 더 쉬운 모델이라고 생각함
      관련 글: Structured Concurrency or Go Statement Considered Harmful
    • Zig의 새로운 std.Io 인터페이스는 Go의 동시성 모델과 유사함
      go 키워드는 std.Io.async, 채널은 std.Io.Queue, selectstd.Io.select로 대응됨
    • Erlang 개발자라면 Go의 동시성 모델이 가장 쉽다는 주장에 동의하지 않을 것임
  • 내가 원하는 건 Go의 단순함에 Rust의 결과/에러/열거형 처리와 더 나은 제네릭을 결합한 언어임

    • 나도 동의함. GC가 있으면서 더 강력한 타입 시스템을 가진 네이티브 언어의 시장 수요가 큼
      OCaml, D, Swift, Nim, Crystal 등을 봤지만 아직 시장을 장악한 언어는 없음
    • 최신 OCaml은 매우 강력한 동시성 모델을 갖추었고, 성능도 Go와 경쟁 가능하다고 들음
    • Borgo 언어는 그런 시도를 했지만 중단됨
      대신 Gleam을 살펴볼 만함
    • Go의 error proposal이 흥미로웠음
      이런 반복되는 문제를 해결할 수 있는 개선이 나오길 기대함
      제네릭은 여전히 어려운 과제일 것임
    • 가장 가까운 대안은 C# 이지만, 여전히 OOP 중심 언어
  • 글의 전반적인 톤이 신입 개발자의 열정과 호기심이 느껴져 좋았음
    Go의 제네릭 부재는 단순한 미니멀리즘이 아니라, 트레이드오프 고민의 결과였다고 생각함
    Rust의 lifetime은 많은 사람에게 가장 큰 난관이었고, 언어의 혁신성은 기존 개념의 조합에 있음
    Zig의 수동 메모리 관리는 OOP 배제보다는 Data-Oriented Design(DOD) 철학에 기반함
    관련 강연: Andrew의 DOD 발표

    • Russ Cox가 2009년에 “The Generic Dilemma” 글에서 제시한 문제처럼,
      “느린 프로그래머, 느린 컴파일러, 느린 실행 중 무엇을 택할 것인가”가 핵심이었음
      Go 팀은 결국 이를 만족스럽게 해결하는 절충안을 찾은 것으로 보임