내가 세계에서 가장 유명한 프로그래밍 언어 중 하나인 Go 언어를 개선하는 데 도움을 준 이야기

발행: (2025년 12월 19일 오전 05:41 GMT+9)
13 min read
원문: Dev.to

Source: Dev.to

소개

모든 것은 약 1 개월 전쯤 시작되었습니다, 그때 나는 사람들이 많이 말하던 Go 언어를 시험해 보기로 결심했어요. 그 언어는 뛰어난 성능을 가지고 있고, 현대적이며, 안전하고, 그 외에도 여러 장점이 있다고 했죠. 그래서 최소 한 번이라도 이 언어를 써보고 싶었습니다. 하지만 언제나 그렇듯 개발자의 삶에서는 일들이 거의 깔끔하게 바로 해결되지 않더라고요 ㅋㅋ.

저는, 여기서 이미 아시는 분들처럼, **C++**를 매우 좋아하고 개인 프로젝트 개발의 대부분을 **C++**로 진행합니다. C++를 여러 해 동안 개발하면서 익숙해진 점 중 하나는 타입 식별자와 변수 식별자가 서로 다른 네임스페이스에 존재한다는 사실입니다. 하지만 Go에서는 그렇지 않았습니다: Go에서는 모든 식별자가 같은 네임스페이스에 존재합니다.

Mas o que é um identificador? E um espaço de nomes? 🤔

간단히 말해서, 식별자는 타입, 변수 또는 함수에 부여되는 이름을 의미합니다. 예를 들어 string 타입의 변수를 senhaDoUsuario라는 이름으로 선언한다면, 타입 이름(string)과 변수 이름(senhaDoUsuario) 모두 식별자로 간주됩니다.

네임스페이스는 직관적으로 타입과 변수 이름이 저장되는 영역(마치 큰 리스트와 같은)이라고 이해할 수 있습니다.

// Exemplo em TypeScript
let nomeDoUsuario: string = '...';
//  ┬────────────  ┬─────
//  │              ╰────▶ Identificador
//  ╰───────────────────▶ Identificador

C++(및 다른 언어들)에서는 타입을 가리키는 식별자와 변수를 가리키는 식별자가 별개의 네임스페이스(두 개의 서로 다른 리스트)에서 존재합니다. 따라서 같은 이름을 가진 경우에도 충돌이 발생하지 않습니다. 즉, pessoa라는 이름의 타입을 만들고 동시에 같은 이름 pessoa를 가진 변수를 선언해도 전혀 문제가 없으며, 컴파일러는 상황에 따라 어떤 것이 타입이고 어떤 것이 변수인지 구분합니다.

Go에서 무슨 일이 일어나나요?

앞서 언급했듯이 Go 언어에서는 모든 식별자가 동일한 네임스페이스에 존재합니다(프로그래밍 언어 이론에서는 이를 flat namespace 라고 합니다). 이는 식별자들이 서로 충돌할 수 있음을 의미합니다. 컴파일을 방해하는 충돌이라기보다는 섀도잉(shadowing) 현상입니다: pessoa라는 이름의 타입을 선언하고 바로 아래에 같은 이름의 변수를 선언하면, 이후 라인에서는 pessoa 타입을 참조할 수 없게 되고 변수가 그 타입을 “가리게” 됩니다.

이 세부 사항이 제 고통의 근원이었습니다.

실용적인 예시: 파일 다루기

제가 처음 언어를 실험했을 때, 간단하지만 사소하지 않은 일을 해보고 싶었습니다: 텍스트 파일 다루기. Go의 흥미로운 점은 오류 처리가 반환값으로 이루어진다는 것입니다; 발생할 수 있는 모든 오류가 함수에 의해 마치 일반 값처럼 명시적으로 반환됩니다 (예외를 던지는 Java와는 달리). 이 때문에 실제 함수 과 실행 중에 발생할 수 있는 오류가 함께 반환되는 패턴이 생깁니다.

// arquivo (simplificado): example.go
value, err := foo()
if err != nil {
    fmt.Println(err)
}
fmt.Println(value)

이 자체만으로도 충분히 편리하고, C 같은 언어에서 왔고 예외 개념을 별로 좋아하지 않는 사람에게는 오히려 선호될 수 있습니다. 문제는 앞서 언급한 사실, 모든 식별자가 같은 네임스페이스에 존재한다는 점을 고려할 때 발생합니다.

이제 foo 함수가 반환하는 값들의 타입을 명시하고 싶다고 가정해 보세요.

// arquivo (simplificado): example.go
var (
    value int
    err   error
)

value, err = foo()
if err != nil {
    fmt.Println(err)
    return
}
fmt.Println(value)

모든 것이 정상적으로 보이죠? 코드가 깔끔하게 컴파일되고, 모든 것이 정상이며, 정말 멋집니다. 하지만 여기서 바로 이 게시물을 쓰게 만든 이유가 숨겨져 있습니다.

변수와 타입 error의 충돌

변수 err타입 error에 같은 이름을 사용했음을 확인하세요. (위 예시에서는 즉시 그림자를 피하기 위해 err를 사용했지만, 같은 이름을 사용하면 문제가 지속됩니다.) 이제 다음 상황을 고려해 보세요:

// arquivo (simplificado): example.go
var (
    value int
    error *error // <-- 변수 이름 "error"
)

value, error = foo()
if error != nil {
    fmt.Println(error.Error())
    return
}
fmt.Println(value)

var (
    anotherValue int
    anotherError error // <-- 타입 "error"인 변수를 선언하려는 시도
)

anotherValue, anotherError = foo()
if anotherError != nil {
    fmt.Println(anotherError.Error())
    return
}
fmt.Println(anotherValue)

무슨 일이 일어나나요?

이 코드를 컴파일하려고 하면 다음과 같은 오류 메시지를 받게 됩니다:

./example.go:28:16: error is not a type

왜 그런가요?

Go에서는 변수 식별자와 타입 식별자가 동일한 네임스페이스를 공유합니다. 위 구문에서 선언

error *error

error라는 변수를 생성합니다. 이 시점부터 error 식별자에 대한 모든 참조는 변수로 해석되며, 사전 정의된 error 타입이 아닙니다. 컴파일러가 다음 줄을 만나면

anotherError error

error타입으로 사용하려 하지만 이미 error 변수에 의해 “그림자 처리”되어 있습니다. 따라서 컴파일러는 error타입이 아니다라고 보고합니다.

결론

  • C++(및 여러 다른 언어)에서는 타입과 변수가 별개의 네임스페이스에 존재하여, 동일한 이름을 동시에 사용할 수 있습니다.
  • Go에서는 모든 식별자가 플랫 네임스페이스를 공유합니다.
  • 변수타입과 같은 이름(예: error)으로 선언하면, 해당 변수는 이후 라인에서 타입을 가리기(섀도잉) 하여 *“identifier is not a type”*와 같은 컴파일 오류가 발생합니다.
  • 가장 간단한 해결책은 미리 정의된 타입과 충돌하는 이름(error, int, string 등)을 피하거나 변수와 타입에 서로 다른 이름을 사용하는 것입니다.

이 세부 사항은 사소해 보일 수 있지만, 저처럼 디버깅에 몇 시간을 소모할 수 있습니다. Go의 플랫 네임스페이스에 주의하고 기존 타입과 충돌하지 않는 변수 이름을 선택하세요!

Source:

의미적으로 잘못된 것으로 간주됨

이제 내 차례다.

오늘, 이 언어에 대한 좌절감 때문에 오랫동안 손을 대지 않다가, 다시 한 번 LinkedIn에 올린 글을 보게 되었습니다. 그 글을 읽고 있던 순간, 나는 Go와 직접 겪었던 그 불쾌한 경험을 떠올렸습니다. 그래서 나는 이렇게 해보기로 했습니다: 왜 언어 레포지토리에 가능한 개선안을 제안하는 issue 를 열지 않을까?

아이디어는 명확했습니다: 아래와 같이 반환되는 대신

./example.go:28:16: error is not a type

아래와 같은 메시지를 반환하면 어떨까?

./example.go:28:16: error refers to a variable, but is being used as type here.
./example.go:16:37: error was re‑declared here as a variable (originally declared as a type at example.go:5:37).

훨씬 나을 겁니다! 앞으로 다른 사람들이 그 신비한 메시지의 의미를 파악하려고 시간을 낭비하는 일을 방지할 수 있겠죠. 적어도 내가 생각한 바는 그렇습니다.

하지만 레포지토리를 살펴보니 열려 있는 issues 가 엄청 많았습니다(5천 개가 넘음). 솔직히 말해 기가 죽었습니다. 이렇게 많은 이슈가 열려 있다면, 이전에 한 번도 기여한 적 없는 무작위 사람인 내가 만든 이슈가 누군가에게라도 눈에 띄기란 매우 어려울 것이라고 생각했습니다.

하지만 그래도 저는 직접 가서 열어봤습니다.

그리고 몇 분 뒤… 답장이 왔습니다! 더 좋은 소식은, 그들이 제안을 받아들였다는 것입니다! 제가 한 비판을 실제로 고려해 주었고, 공식 다음 릴리즈의 개선 목표로 백로그에 올려 주었습니다. 그래서 언젠가 먼 미래에 여러분이 이 언어를 사용하면서 같은 상황을 마주한다면… 이미 더 나빠진 적이 있다는 것을 알게 될 겁니다!

이게 전부입니다! 이제 (이력서에 👀 넣을 수도 있겠죠) 어느 정도는 현재 세계에서 가장 유명한 언어 중 하나에 기여했다는 말을 할 수 있겠네요! ㅋㅋㅋㅋ.

기여라는 개념을 조금 과장했을 수도 있지만, 어쨌든 의도만큼은 충분히 가치가 있지 않나요! 😁

Back to Blog

관련 글

더 보기 »