1P by GN⁺ 12시간전 | ★ favorite | 댓글 1개
  • Go 언어 설계의 여러 결정들이 불필요하거나 기존 경험을 무시한 채 이루어졌음
  • 에러 변수의 범위 관리 문제가 코드 가독성과 버그 탐색을 어렵게 만듦
  • nil의 이중성, 메모리 사용, 코드 포터블성 등 여러 부분에서 비직관성 및 현실과 맞지 않는 설계 나타남
  • defer 구문의 한계 및 표준 라이브러리의 예외 처리 방식이 예외 안전성 확보를 어렵게 만듦
  • 메모리 관리 및 UTF-8 처리 부실 등 누적된 문제들이 Go 코드베이스 품질에 장기적으로 악영향을 주고 있음

Go 언어에 대한 장기적 비판

에러 변수 범위의 비직관성

  • Go의 문법은 에러 변수(err)의 범위를 불필요하게 넓혀 오류 발생 가능성을 높임
    • 예시 코드에서 err 변수가 함수 전체에 살아있고, 재활용되며, 이로 인해 코드 가독성과 유지보수성이 저하됨
    • 숙련된 개발자는 이런 범위 문제로 인해 버그 탐색에서 오해와 시간 낭비를 경험함
    • 변수를 적절히 지역적으로 한정할 수 있는 방법이 문법적으로 허용되지 않음

두 가지 형태의 nil

  • Go에는 interface 타입과 포인터 타입 각각에서 nil이 다르게 동작하는 혼란이 존재함
    • 아래 예시처럼 s(포인터)와 i(interface)에 nil이 할당되어도, s==i는 다르게 평가되는 등 일관성 없는 동작을 보임
    • 이는 null 처리에서 일반적으로 피하고 싶은 문제로, 설계상의 충분한 고민 없이 만들어진 흔적임

코드의 이식성 한계

  • 조건부 컴파일을 위한 주석 사용은 유지보수성과 포터블성 면에서 현저히 비효율적임
    • 실제로 포터블 소프트웨어를 만들어본 경험이 있다면, 이러한 방식이 번거롭고 오류를 유발함을 알 수 있음
    • 역사적으로 쌓은 경험(코드 포터블성, 실무적 사례들)이 무시됨
    • 자세한 내용은 Go programs are not portable을 참고

append의 소유권 불명확성

  • append 함수와 슬라이스의 소유권 관계가 명확하지 않아 코드 예측이 어려움
    • 예제를 통해, foo 함수에서 슬라이스를 append 시켰을 때 실제로 원본에 어떤 영향이 있는지 미리 알기 어려움
    • 언어의 알아두어야 할 ‘quirk’들이 늘어나 실수를 야기함

defer 구문 설계 미흡

  • RAII(Resource Acquisition Is Initialization) 원칙처럼, 자원 해제를 명확히 지원하지 않음
    • Java와 Python의 구조적 자원 관리 구문 대비, Go는 어떤 자원이 defer로 해제되어야 하는지 명확히 알 수 없음
    • 예시처럼 파일 작업 시, double-close 문제까지 직접 다루어야 하며, 올바른 해제 순서와 방식이 불분명함

표준 라이브러리의 예외 처리

  • Go는 명시적 예외(exception)를 지원하지 않는 구조지만, panic 등 예외 상황은 여전히 발생함
    • panic이 일부 상황에서 완전히 프로그램을 종료하지 않고 잠식하는 경우도 있음
    • 표준 라이브러리(fmt.Print, HTTP 서버 등)에서 예외를 무시하는 패턴이 존재해, 진정한 예외 안전성 보장이 불가
    • 결국 예외 안전 코드 작성은 필수이나, 직접적으로 예외를 사용할 순 없음

UTF-8 처리와 문자열

  • string 타입에 임의의 바이너리 데이터를 넣어도 Go는 특별한 검증 없이 작동함
    • 과거 UTF-8 인코딩 이전에 생성된 파일명 등이, 조용히 누락되는 사례를 겪을 수 있음
    • 백업 등에서 중요 데이터가 손실될 수 있으며, 실무 상황을 반영하지 않은 간단한 처리 방식임

메모리 관리의 한계

  • RAM 사용량에 대한 직접적 제어가 어렵고, GC(가비지 컬렉션)의 신뢰성도 한계가 있음
    • Go의 메모리 사용량이 증가해 장기적으로 비용 및 성능 이슈로 연결됨
    • 여러 인스턴스, 컨테이너 환경에서 비용 및 확장성 문제가 실제로 발생함

결론: 더 나은 길이 있었음

  • 이미 기존에 효과적으로 입증된 언어 설계들이 있었음에도 불구하고, Go는 많은 부분에서 그것을 외면함
    • Java 초기안의 문제점들과는 달리, Go가 출시될 당시 이미 더 나은 접근법이 있었음

참고 자료

Hacker News 의견
  • Go를 pre-1.0 시절부터 거의 모든 풀타임 직장에서 써왔음. 팀원들이 기본을 배우기에 단순하고, 대체로 안정적으로 동작함. Go 최신 버전으로 업데이트할 때 걱정할 일이 거의 없으며, 대부분 유용한 기능이 기본적으로 포함되어 있음. 컴파일 속도가 빠른 점이 매력적임. 병렬 처리는 약간 까다롭지만, 시간을 투자하면 데이터 흐름을 표현하기 좋아짐. 타입 시스템은 대부분 편리하지만 가끔은 장황한 면이 있음. 전반적으로 믿을 만한 도구임. 하지만 기사에서 언급된 여러 지적에 공감하게 됨. Go는 구세대 개발자들이 너무 원칙에만 집착해서 실용적 편의를 놓친 부분이 분명히 있음. 물론 이건 내 느낌이고, 모든 단점을 다 해결했다면 지금보다 더 안 좋았을 수도 있다고 생각함. 최근 몇 년 사이에는 쿼크 수정에 더 열려진 분위기가 느껴진다는 점도 언급하고 싶음. 한때 제네릭이나 커스텀 이터레이터가 추가될 줄은 상상도 하지 못했음. RAM과 이식성에 관한 지적은 다소 개인적 불만처럼 느껴짐. 개선되면 좋겠지만, GC가 대부분의 프로그램에서 심각한 문제를 일으키는 경우는 극히 드물고, 디버깅 또한 어렵지 않음. 그리고 Go는 거의 모든 중요한 플랫폼을 지원함. 다만, 에러와 nil 처리 방식에 계속 불편함을 느낌. Result[Ok, Err], Optional[T] 같은 구문이 자주 그리워짐

    • 나는 오히려 Go가 원칙에 고집한 게 아니라, 당장 눈앞에 보이는 문제를 빠르게 해결하는 편의성에 집착했다고 봄. 문제를 근본적으로 분석해서 올바르게 해결한 게 아니라, ‘Not Invented Here’ 정신을 버리고 즉석에서 툭툭 만들어 쓴 느낌임. Go의 파일시스템 API가 대표적인 예시임. 파일 여는 함수가 필요하면 단순히 func Open(name string) (*File, error) 이런 식으로 만들고 끝. 그런데 파일 이름이 UTF-8이 아니면? 5년 동안 그 문제가 안 생기니까 신경 안 씀

    • Go의 설계 원칙이 “컴파일러를 쉽게 만들고 컴파일을 빠르게 하자”라는 목표에만 너무 치중된 느낌을 자주 받음. 개발자 편의성보다는 컴파일러/컴파일 자체에만 초점이 맞춰진 구조임

    • 20년 만에 컴파일된 언어로 새로운 직장서 Go를 처음 제대로 써봄. 개인 취향이겠지만 솔직히 쓰면서 불쾌한 감정도 듦. 기본 인자값이 없음, 에러 처리 방식이 마음에 안 듦, 프로덕션에서 제대로 된 스택트레이스가 없음. 객체지향 문법은 각 함수에 어색한 참조를 붙여야 해서 보기 안 좋음. 포인터도 부담임. 결국 C/C++ 옛날 기술로 돌아간 느낌임. 대학 시절 1999년 즈음에 했던 프로그래밍 분위기가 그대로임

    • Go는 병렬 처리 측면에서, 내 경험상 다중 코어 CPU 환경에서 언어 자체로 병렬을 자연스럽게 다루는 유일한 시스템임. CSP 스타일의 goroutine/channel 공식 덕분에 병렬 처리 로직이 직관적으로 표현됨. Python은 GIL과 난해한 async 라이브러리들로 골치가 아픔. C, C++, Java 등은 언어 외 추가 라이브러리가 필요해서 언어 수준에서 병렬 처리 추론이 쉽지 않음. 그래서 HTTP 서버나 서비스용으로 go가 완벽하게 어울린다고 봄. 내 경험상 이만한 대안이 없음

    • 개발자 관점에서 ergonomics, 즉 표준화·일관성 면에서는 완벽했다고 느낌. 여러 마이크로서비스 코드베이스에서도 스타일이 다를 걱정이 없고, 포맷 논쟁도 필요없음. 다만, Go만의 표준 방식을 선택할 때 조금 옛날 스타일을 너무 고집한 것 같음. 요즘 개발자들은 map/filter 같은 함수형 메서드를 더 기대하는데, Go는 인덱스 실수 위험이 있는 반복문만 제공함. TypeScript 수준으로 똑똑한 타입 시스템도 아님. 에러 처리는 불편함. 이러한 기능을 추가하면 “창의적이지만 안 좋은 사용법”이 늘어날 우려는 이해하지만, JS 세대에게는 go를 설득하기가 어렵다는 점도 실감함

  • 5년 넘게 대형 Golang 프로젝트에 전념해왔는데, 메모리 사용을 최소화해야 하는 컴포넌트를 만들다 보면 Go의 허술한 부분과 자주 마주침. GC가 빠르게 청소하지 않거나 힙 조각화 문제가 심해짐(Go가 압축형 가비지 컬렉터가 아니라서 생기는 문제). 이 때문에 할당을 완전히 피해가려고 노력하지만 버그가 발생하기 쉬움. 디버깅도 극도로 까다로움. 힙 프로파일을 떠봐도 살아남은 객체 정보만 보여주고, 실제 쌓인 쓰레기나 조각화 내역은 보이지 않아 추측에 의존해야 함. 예를 들어, X 함수가 힙 1KB만 할당했다고 나오지만 반복문에서 계속 호출되면 수십 MB 쓰레기가 발생함. 그래서 미리 정적 버퍼를 할당해 재활용하는데, 소유권 문제가 복잡해지고 append와 같은 허점이 생김. 표준 라이브러리도 직접 다시 구현해야 하기도 함. 우리 경우가 일반적이지는 않다는 것도 알지만, 정말 언어와 싸우는 느낌이 들어서 아쉬움

    • 이런 경우에는 오히려 메모리를 heap 밖으로 옮기는 게 덜 고통스러울 수 있음. 물론 GC 언어니까 쉽지는 않지만, 너무 C++/Rust스러운 코드를 Go로 억지로 구현할 바에는 그 부분만 해당 언어로 아예 바꾸는 게 좋음

    • 이런 상황에 go 언어를 선택한 게 언어 선택 자체가 문제였다고 생각함. C/C++/Rust/Zig가 더 적합하다는 의견임

    • 새로운 "Green Tea" 가비지 컬렉터가 도움이 될 수도 있다는 소식이 있음. 이는 메모리 중심은 아니더라도 메모리 인접 객체를 더 잘 처리하는 병렬 마크 알고리즘임. 관련 정보는 여기에서 참고 가능함

    • arena 실험이 진행 중이었지만 현재는 중단된 상태임. 그래도 관심 가질만함

    • 이게 도움되는 말은 아니라 미안하지만, 현재 상황을 보면 언어 선택이 완전히 잘못됐다고 생각함. 아마 회사 내 공식 언어 정책 때문에 억지로 go를 쓰는 게 아닌가 추측함. 대기업들이 널리 쓰는 언어로만 프로덕션을 승인하는 경우가 흔함

  • Go의 defer가 함수 스코프(범위)에만 동작하고 어휘 스코프에는 적용되지 않는 이유를 아직도 이해 못하겠음. 이 사실을 알게 된 계기도 반복문 안에서 파일을 처리하다가 파일 리스트가 커지니까 defer가 함수 종료까지 핸들을 닫지 않아 크래시가 발생한 경험 때문임. 주변 Go 개발자들은 반복문 본문을 익명 함수로 감싸서 쓰라 했음. 그밖에는 자잘한 점 몇 가지 빼고는, Go가 쾌적하게 느껴지며, 효율적인 문법에다 쓸데없는 ‘뽐냄’ 문화를 막아주기도 함. C# 프로젝트를 Go로 큰 규모로 리라이트해봤는데, 기능이 10분의 1임에도 코드가 더 적었음. GC 할당 강제가 아니라 성능 좋은 기본값을 사용하도록 유도되고, serialization 같은 작업에 내장 코드 생성 기능이 편리함. ORM처럼 모든 것을 언어로 대체하려는 C# 문법과 달리, Go에서는 SQL은 그냥 SQL로 쓰고, gRPC도 protobuf 스펙으로 처리하는 분위기임

    • 때로는 어휘 스코프에 defer가 필요하고, 때로는 함수 스코프가 필요함. 예를 들어, 반복문에서 여러 파일을 열어 함수가 끝날 때까지 모두 열어두고 싶으면 함수 스코프가 필요함. 현재는 함수 스코프지만 어휘 스코프가 필요할 때는 func로 감싸면 됨. 만약 어휘 스코프만 지원하고 함수 스코프가 필요하면 어떻게 해야 할지 모호함

    • 래핑용 함수 없이 한 단계 들여쓰기를 줄인다는 점, 동작이 콜 스택이나 스택 언와인딩과 연관된다는 점, C의 ‘goto fail’ 스타일에서 보면 자연스럽다는 점 등이 장점임. 물론, 반복문에서 defer 쓸 땐 별도로 함수로 감싸야 해서 좀 불편함

    • 블록 레벨 언어와 함수 레벨 defer를 둘 다 써본 경험이 있는데, 가끔 조건문 안에서도 함수 레벨 defer를 쓸 수 있었으면 하는 바람이 생김

    • 특별히 깊은 이유가 있을 것 같진 않고, 실제로 중요하냐는 생각임

    • C#에서도 SQL이나 protobuf 스펙으로 작업할 수 있음. 단지 다른 선택지도 있다는 차이임

  • Go에도 단점은 많지만, 서버 사이드 언어 범주에서 이 정도로 균형 잡힌 언어는 없다고 느낌. Node나 Python보다 빠르고, 타입 시스템도 더 낫다고 생각함. Rust보다 진입장벽이 낮고, 표준 라이브러리와 툴링도 훌륭함. 단순한 문법, 하나의 방식만을 강제하는 부분도 마음에 듦. 에러 처리는 문제가 있지만, Node처럼 catch에 아무 에러나 들어오는 것보다는 나음. 혹시 이 기준을 모두 충족하는 더 좋은 언어가 또 있나 궁금함. 스스로 Go 광신자는 아니고, 경력 내내 Node로 백엔드를 많이 했지만 최근 Go를 실험적으로 써보는 중임

    • 사실 이 모든 장점을 Java나 C#에도 동일하게 쓸 수 있을 것 같음

    • 'Node'를 프로그래밍 언어라 부르는 게 거슬리기는 함. Node는 JavaScript 런타임이고, 요즘 Node로 실행되는 프로젝트 상당수가 TypeScript로 작성됨. 즉, Node라고 해도 사용 언어가 명확하지 않다는 것임. TypeScript를 기준으로 본다면 오히려 Go 타입 시스템보다 더 생산적이라고 느낌. Rust와 비교해도 동일한 주장을 할 수 있음

    • 대다수 언어가 나름대로 불편한 점이 있음. Go는 퍼포먼스와 포터빌리티, 런타임/에코시스템도 우수함. 반면, nil 포인터, zero value, 소멸자 없음, 매크로 없음 등의 단점도 있음(Go의 매크로 부재를 회피하려고 코드 생성이 남용됨). 더 좋은 언어도 있지만(예: Rust), 그런 경우는 Go보다 훨씬 복잡해짐. 그 이유는 Go 제작자가 단순함을 최우선으로 해서 발생하는 문제임

    • 최근 Python의 타입 시스템 발전을 감안하면, Go보다 훨씬 앞선다고 봄. structural typing만 놓고 비교하면 Python이 더 인상적임

    • Go 타입 시스템이 많이 부족하다고 생각함

  • Go로 만든 static site generator를 확장한 적이 있었는데, 코드는 매우 명확하고 읽기 쉬웠지만 언어적 허점 때문에 확장성이 낮았음. 단순한 변경도 코드 여러 군데를 어렵게 뜯어고쳐야 했음. 다양한 수준의 캡슐화와 추상이 어렵고, ‘단순함’을 위해 추상화가 희생됨. 추상화가 쉬운 코드(확장 가능한 코드)를 만드는 가장 중요한 방법인데, Go는 확장성 대신 단순성을 택함. 대체로 Go 프로그램은 “확장성 없는 단순함”에 그치는 느낌임. 사람들은 Go는 그런 언어라고 우기지만, 내 경험에는 납득이 가지 않음. 그나마 ‘개발 경험’만은 나쁘지 않음

    • Go에 대한 대화가 항상 묘하게 느껴짐. 비판하면 대부분 “그 언어 원래 그래”라며 그냥 받아들이라는 분위기가 생김. 단순함을 강점이라 하지만, map의 키 리스트를 뽑으려면 반복문을 직접 짜야 하는 게 과연 더 단순한가 의문임

    • Go를 잠깐 써보고서 이런 식의 비판을 쉽게 내릴 수 있냐고 묻고 싶음. 나는 2015년부터 수많은 대형 Go 코드베이스(수백만 줄)를 경험했고, 여러 팀에서 일했음. Go의 확장성은 C, C#, Java와 비교해서 특별히 떨어지지 않음. Go는 표현력보다는 명확함을 택하는 성향이 강함. 그래서 추상화 레이어가 적고, 더 구체적이고 명시적으로 짜는 습관을 들이게 됨. 하지만 그게 확장 불가능으로 이어진다고 보지 않음. 모듈형 확장 가능한 설계는 언어가 아니라 개발자가 학습해서 이루는 영역임. 네가 다룬 코드가 잘못 설계돼 있었을 뿐 Go 언어의 한계는 아님

  • Go를 몇 년간 썼고, 작은 것은 빠르게 만들 수 있지만, 규모가 커질수록 수많은 작은 불편함에 시달리게 됨. 디버깅이 특히 악몽 같은데, 사용하지 않은 X가 있으면(디버깅 중 특정 부분을 주석 처리했을 때 항상 발생) 컴파일이 아예 안 됨. 불필요한 양식화, 특별한 파일명, 예약된 필드명이 많은 것도 번거로움. 표준 라이브러리에 숨어있는 panic, 예상치 못한 heap 복사도 느리고 성가심. Go의 ‘마법’스러운 부분은 대부분 기존 기능(특수 파일명, 대소문자 등)을 억지로 차용해서 생긴 부작용임. 정말로 “public”처럼 노출하려면 pub를 쳐도 됐을텐데 이상하게 고집스러움. 요즘 AI가 좋아져서 Rust에서 타입 문제나 빌림 체크 문제가 생기면 AI에게 바로 물어보고 빠르게 해결할 수 있어서 훨씬 즐거움. 예전처럼 문서나 SO 뒤지며 시간을 낭비하지 않아도 됨

    • 최근 Rust를 본격적으로 해보진 않았지만, 지난 12월에 잠깐 써봤을 때 AI가 Rust에 너무 잘 대응한다고 놀랐음. 자세한 문법과 명시적 타입 정보가 많으니 오히려 사람보다 AI가 더 잘 풀음

    • Go에서 디버깅 중 컴파일 에러가 나는 문제로 불만을 토로하면 Go 진영에서는 “제대로 도구를 쓰라”고 질책함. 원칙을 너무 극단적으로 적용해서 불편함

    • 이 디버깅 관련 불편을 Go의 창시자에게 말해본 적 있는데, 그조차 문제를 이해하지 못하더라. 너무 아마추어 같아서 실망스러웠음. 참고로, AI가 Go는 오히려 잘 못 다룸. 언어가 단순한 편인데도 ChatGPT가 Java, C#, Python을 더 잘 지원함

  • 개인적으로 Go를 좋아하지 않고 결정적인 단점도 많이 보이지만, 인기가 계속 높은 이유는 분명함. Go는 비교적 빠르면서, goroutine 기반 덕에 멀티스레드 없이도 안정적이고 신뢰성 있는 고병렬 서비스를 쉽게 쓸 수 있음. 구글이 Go를 내놓았을 때 비슷한 대중적, 정적, 컴파일 언어가 거의 없었음. 지금도 비슷한 위치에 있는 경쟁자는 Java(이제 virtual thread 지원) 뿐임. async/await 지원 언어들이 비슷한 약속을 하지만 실제론 복잡성이 많음(비동기 태스크에서 블로킹 피하기, 함수 색칠 등). Erlang도 범주가 다름. 결국 단점이 많음에도 goroutine과 Google 프로젝트의 네임밸류로 인기가 높은 편임

    • 점진적으로 JVM이 Go와의 격차를 줄이고 있음. virtual threads, zgc, lilliput, Leyden, Valhalla 같은 프로젝트를 통해 점점 나아짐. Java 8에서 25까지 변화는 엄청남. 앞으로는 더 편리해질 것 같음

    • Go의 명시성과 단순함이 LLM 보조 프로그래밍에 매우 적합함. 옛날 Go 1.x 코드도 그대로 최신 버전에서 잘 동작함

    • 실제로 구글 내부에서는 virtual threads 도입한 Java를 Go보다 훨씬 더 자주 씀

    • 새로운 프로젝트에 가장 어울리는 “현대적 언어”가 무엇이라 생각하는지 궁금함

  • 1.0 출시 전부터 Go를 좋아했지만, “아직도 못 만들었다”라는 평가는 공감이 안 감. 물론 단점이나 불만은 있지만, 창시자가 프로젝트를 떠나면 중앙 비전 유지가 힘들어지고 언어가 나빠질 위험도 있다고 생각함. “서버 언어”로만 포지셔닝된 것도 결국 Rust나 Python 등으로 떠나게 만들 거라 봄. Visual Basic도 까던 시절이 있었지만, 결국 필요한 사람들은 잘 쓰던 시절이 있었음

  • Go의 단점을 다룬 비판 글들이 실제로 꼼꼼히 따져보면 크게 문제로 여길 내용이 아님. 대부분은 기술적으로 맞지만 사소한 부분임. 반대로 정말 심각한 언어 설계 문제는 zero value, 생성자 미지원, null 처리 부실, 기본 mutability, 제네릭을 염두에 두지 않은 타입 시스템, 임의 정밀도를 지원하지 않는 int, 소유권 문제가 애매한 slice 등임(관련 이슈 1, 관련 이슈 2). sum type 없음, 문자열 보간 미지원 등도 단점임

  • Go로 책을 쓸 정도로 편향되어 있을 수 있지만, 10년 넘게 Go를 써온 입장에서 처음엔 정말 신선함을 느꼈음. Java보다 상용구가 적고, 익히기 쉽고, 성능도 무난함. 최고의 언어는 없고, 용도마다 최선의 선택이 있지만, 전형적인 백엔드 작업에는 후회 없는 선택임

    • 집에서 DIY나 목공에 Dremel을 즐겨쓰듯이, Go도 쉽고, 부담 없는 도구임. Dremel은 접근성이 좋아서 큰 톱을 쓸 필요 없이 금방 작업할 수 있음. 하지만 Dremel만 쓰다 보면 작업이 5배 오래 걸리고 결과물이 엉성할 수 있음. 결국 제대로 된 결과가 중요한 경우, 올바른 도구를 골라야 한다는 점을 잊지 않으려 ‘Dremel 효과’라고 부르며 스스로를 경계함