# Python 대신 Go로 스크립팅하기

> Clean Markdown view of GeekNews topic #25455. Use the original source for factual precision when an external source URL is present.

## Metadata

- GeekNews HTML: [https://news.hada.io/topic?id=25455](https://news.hada.io/topic?id=25455)
- GeekNews Markdown: [https://news.hada.io/topic/25455.md](https://news.hada.io/topic/25455.md)
- Type: GN+
- Author: [neo](https://news.hada.io/@neo)
- Published: 2025-12-31T09:38:35+09:00
- Updated: 2025-12-31T09:38:35+09:00
- Original source: [lorentz.app](https://lorentz.app/blog-item.html?id=go-shebang)
- Points: 23
- Comments: 1

## Summary

Go 파일을 **직접 실행 가능한 스크립트처럼 사용하는 기법**을 소개합니다. 첫 줄에 `//usr/local/go/bin/go run "$0" "$@"; exit`를 넣고 실행 권한을 주면, 커널의 ENOEXEC 폴백 동작을 이용해 `./script.go` 형태로 실행할 수 있습니다. 이 방식은 shebang이 아니라 셸의 예외 처리 흐름을 활용한 것으로, Go 컴파일러가 해당 줄을 주석으로 무시하기 때문에 가능합니다. 이를 통해 별도의 가상환경이나 패키지 매니저 없이도 **Go의 표준 라이브러리와 강한 하위 호환성** 덕분에 장기적으로 유지 가능한 스크립팅 환경을 만들 수 있습니다.

## Topic Body

- Go 파일을 **실행 파일처럼 직접 실행**할 수 있는 트릭 소개  
- 첫 줄에 `//usr/local/go/bin/go run "$0" "$@"; exit`를 두고 실행 권한을 주면 `./script.go`로 실행 가능  
- 이 방식은 shebang이 아니라 **POSIX에서 ENOEXEC 발생 시 셸이 /bin/sh로 폴백하는 동작**을 이용함  
- 셸은 첫 줄을 명령으로 실행하고, Go 컴파일러는 `//` 주석으로 인식해 무시함  
- `"$0"`로 자기 자신 경로를 넘겨 `go run`이 스크립트를 빌드·실행하고 `$@`로 인자 전달  
- Go의 강력한 **표준 라이브러리와 하위 호환성 보장**이 스크립팅 용도에 적합하며, Go 1.x 버전을 사용하는 한 스크립트가 수십 년간 동작 가능  
- Python의 가상 환경, pip/poetry/uv 등 **의존성 관리 복잡성을 피할 수 있음**  
  
---  
  
### 가짜 shebang의 작동 원리  
  
- **shebang(`#!`)은 execve 시스템 콜을 통해 인터프리터를 지정**하는 방식이지만, 이 글에서 소개하는 기법은 shebang이 아님  
- Go 소스 파일 첫 줄에 `//usr/local/go/bin/go run "$0" "$@"; exit`를 두고 `package main` 이하에 일반 Go 코드를 두는 형태  
  - `chmod +x script.go`로 실행 권한을 주면 `./script.go`처럼 실행 가능  
- `strace`로 확인해보면, 셸이 `./script.go`를 execve로 실행 시도할 때 **커널이 ENOEXEC(Exec format error)를 반환**  
  - ENOEXEC를 받은 셸은 **fallback으로 `/bin/sh`를 사용해 해당 파일을 셸 스크립트로 해석**  
  - 셸에서 `//`는 주석이 아니라 **루트 경로(`/`)로 해석**되므로 `//usr/local/go/bin/go`는 정상적인 경로로 실행됨  
- 따라서 첫 줄 `//usr/local/go/bin/go run "$0" "$@"; exit`가 셸에서 명령으로 실행됨  
  - `"$0"`는 실행된 파일 경로를 전달하니까, 실행 명령에서 `"$0"`가 `script.go` 경로가 되어 `go run`이 **자기 자신을 찾아 빌드·실행**하는 형태가 됨  
  - `"$@"`는 1번 인자부터의 위치 인자 확장으로 `./script.go -f flag0 here are some args` 같은 호출을 가능하게 함  
  - `; exit`가 없으면 `sh`가 Go 파일을 계속 줄 단위로 해석하다가 `package` 같은 토큰에서 오류가 남   
  
### Go가 스크립팅에 적합한 이유  
- Go의 **하위 호환성 보장**이 핵심 기능으로, Go 1.x를 사용하는 한 작성한 스크립트가 장기간 동작함  
- **잘 발달된 표준 라이브러리**와 내장 도구(포맷터, 린터 등)가 별도 설정 없이 제공되어, **스크립트 공유와 이식성이 극대화**됨  
  - Python처럼 **가상 환경이나 다양한 패키지 매니저(pip, poetry, uv)를 배울 필요 없이** 코드 실행 가능  
  - Go 생태계의 내장 도구와 IDE 연동으로 `.pyproject`나 `package.json` 없이도 포맷터·린터를 기본적으로 쓸 수 있음  
- 최신 Go만 설치되어 있으면 **어떤 OS에서든 수십 년간 실행 가능**  
  
### 다른 컴파일 언어와의 비교  
- **Rust**는 컴파일 속도가 느리고 표준 라이브러리가 약해 의존성 사용이 필수적이며, 완벽함을 요구해 개발 속도가 느림  
- **Java 및 JVM 언어**는 이미 JVM 바이트코드 기반 스크립팅 언어가 존재하며, [**경량 Kotlin 스크립팅**도 대안이 될 수 있음](https://kotlinlang.org/docs/custom-script-deps-tutorial.html)  
- Go는 컴파일 언어 중 **스크립팅 용도에 가장 적합한 특성**을 보유  
  
### gopls 포맷팅 문제와 해결책  
- `gopls`는 주석 뒤에 공백을 요구(`//example` → `// example`)하므로 **가짜 shebang 라인이 깨짐**  
- 공백이 들어가면 `// usr/local/go/bin/go`가 되어 셸에서 경로로 인식되지 않음  
- **해결책**: [HN 스레드의 제안](https://news.ycombinator.com/item?id=46433605)으로 `//` 대신 `/**/` 블록 주석 사용  
  - `/*usr/local/go/bin/go run "$0" "$@"; exit; */` 형태로 작성  
  - **`exit` 뒤에 세미콜론(`;`)이 필수**

## Comments



### Comment 48499

- Author: neo
- Created: 2025-12-31T09:38:36+09:00
- Points: 1

###### [Hacker News 의견들](https://news.ycombinator.com/item?id=46431028) 
- 작성자가 말한 “pip vs poetry vs uv 신경 안 씀” 부분은 사실 **uv**가 이 사용 사례를 직접 지원함  
  PyPI 의존성까지 포함해서, Python 버전과 uv만 설치되어 있으면 됨  
  [uv 공식 문서 링크](https://docs.astral.sh/uv/guides/scripts/#using-a-shebang-to-create-an-executable-file)
  - 이보다 더 나은 방법도 있음  
    `#!/usr/bin/env -S uv run --python 3.14 --script`  
    이렇게 하면 Python 자체가 설치되어 있지 않아도 uv가 지정된 버전을 내려받아 실행함
  - 나도 그렇게 생각했지만, **비-Python 사용자**에게는 아직 직관적이지 않음  
    Clojure를 처음 접하면 대부분 Leiningen을 쓰라는 조언을 듣지만, Python은 검색하면 venv, poetry, hatch, uv 등 여러 가지가 나옴  
    uv가 점점 대세가 되어가고 있지만 아직은 보편적이지 않음  
    예전에 Go를 apt로 설치했다가 버전이 너무 오래돼서 다시 설치했던 경험이 있는데, 그건 훨씬 빨리 해결됐음  
    Python의 가상환경 문제는 여전히 복잡함
  - 나는 2019년에 **PyFlow**로 이 문제를 해결했었음  
    Rust로 작성된 OSS 도구로, Python 버전과 venv를 자동 관리함  
    `pyproject.toml`만 설정하고 `pyflow main.py`를 실행하면 Cargo처럼 의존성을 설치하고 잠그며, 프로젝트에 맞는 Python 버전도 자동으로 맞춰줌  
    당시에는 Poetry와 Pipenv가 인기였지만, venv와 버전 관리까지는 부족했음
  - 나도 대부분 **uv**로 옮겨감  
    `uv add`를 주로 쓰고, 필요할 때만 `uv pip`를 씀  
    하지만 `uv pip`는 pip의 한계를 그대로 가짐 — 설치 순서에 따라 의존성 해석이 달라짐  
    `uv pip install dep-a` 후 `dep-b`를 설치하는 것과 순서를 바꾸는 것, 혹은 한 번에 설치하는 것이 다 다름  
    이는 pip의 문제에 가깝지만, Python 패키지 관리의 **혼란**은 여전함  
  - 사실 Python 버전조차 지정할 필요 없음  
    uv가 알아서 내려받음

- Go는 **shebang 지원을 명시적으로 거부**했음  
  대신 `gorun`을 쓰는 게 권장됨  
  `/// 2>/dev/null ; gorun "$0" "$@" ; exit $?` 같은 POSIX 트릭으로 실행 가능함  
  Nim, Zig, D는 `-run` 옵션으로 비슷하게 쓸 수 있고, Swift, OCaml, Haskell은 파일을 직접 실행할 수 있음  
  [관련 토론 링크](https://groups.google.com/d/msg/golang-nuts/iGHWoUQFHjg/_dbLKomrPmUJ)
  - 작은 스크립트에는 `go run` 대신 **yaegi** 인터프리터가 더 나을 수도 있음  
    [yaegi GitHub](https://github.com/traefik/yaegi)

- “pip, poetry, uv 차이를 알고 싶지 않다, 그냥 코드만 실행하고 싶다”는 글은 결국 **기술 숙련도 문제**임  
  `uv run`과 **PEP 723**이 이미 모든 문제를 해결했음
  - 맞음, 하지만 `uv run`이 나오기까지 너무 오래 걸렸음  
    20년 넘게 Python을 써왔지만, 외부 패키지나 venv가 있는 코드베이스는 항상 두려웠음  
    `uv run` 덕분에 회사 프로젝트는 모두 옮겼지만, 개인 프로젝트는 이미 Go로 넘어감  
    장기적으로는 **정적 타입 언어**를 선호함  
  - 오래된 언어라면 결국 경쟁 라이브러리를 배우게 됨  
  - 이건 **UX 문제**임  
    사용자는 그냥 프로그램이 실행되길 원함  
    `uv run`과 PEP 723이 문제를 해결했지만, uv를 알아야 한다는 점에서 여전히 진입장벽이 있음  
    uv가 공식 기본 도구가 아닌 이상, 많은 사용자가 Python을 떠날 것임

- 정말 **천재적인 아이디어**라고 생각함  
  하지만 스크립팅은 배포용 소프트웨어와는 다른 **인체공학적 감각**이 필요함  
  bash는 즉흥적이고, Go는 제품화에 적합하며, Python은 그 중간쯤, Ruby는 bash에 가깝고, Rust는 Go 쪽임  
  스크립트는 OS 명령어를 빠르게 조합해 일회성 작업을 처리할 때 유용함  
  Go는 그런 즉흥성이 부족함  
  - 나도 Python이 “중간쯤”이라는 말에 공감함  
    Debian에서 간단한 gtk 앱을 uv로 실행하려 했는데, 의존성은 다 맞는데도 실행이 안 되고 결국 **Core Dump**  
    Python을 새로 시도할 때마다 이런 일이 반복됨  
    Go는 장황하지만, 한 번 컴파일되면 그냥 동작함  
  - 나도 비슷하게 느낀다  
    핵심은 한 파일로 끝낼 수 있느냐임  
    Go로도 500줄짜리 스크립트는 가능하지만, 언어 자체가 여러 파일과 모듈을 전제로 함  
    bang-line이 안 되는 것도 그런 이유임  
    어차피 `go run`이 임시 바이너리를 만드는 구조라면, 그냥 빌드해서 `/usr/local/bin`에 넣는 게 낫다고 봄  
  - bash가 OS 명령어에 더 가깝다는 말은 오해임  
    bash도 Python만큼이나 OS 위의 추상 계층일 뿐, 단지 기본 셸이라 그렇게 느껴질 뿐임  
  - LLM이 코드를 대신 수정해주는 시대라면, 작성 인체공학보다 **가독성**이 더 중요해질 수도 있음  
    특히 LLM이 쓴 코드를 사람이 읽기 쉽게 만드는 방향으로

- Python을 처음 접하는 사용자가 pip, poetry, uv 차이를 몰라도 된다는 점에는 동의함  
  하지만 이런 주제로 글을 쓰는 블로거라면 최소한 **uv가 문제를 해결한다는 사실** 정도는 알아야 함  
  무지한 비판은 설득력이 없음  
  - uv가 Go처럼 “write once, run anywhere”를 해결하냐는 질문이 있음  
    나도 uv의 개념을 완전히 이해하지 못했기에 궁금함

- 나는 Python으로 스크립트 짜는 걸 **좋아함**  
  빠르게 작업할 수 있고, 타입이나 메모리 걱정 없이 단순한 일을 처리하기 좋음  
  하지만 대규모 애플리케이션에는 쓰고 싶지 않음  
  - 나도 Python 스크립팅은 좋아하지만, **남의 스크립트를 설치**하는 건 싫음  
  - 이건 Linux 중심의 접근임  
    대부분 시스템에 기본 Python이 있어서 간단한 스크립트엔 충분함  
    Go를 설치해야 하는 점을 생각하면, 차라리 uv로 Python을 쓰는 게 낫다고 봄  
    글쓴이도 “약간 트롤링으로 시작했다”고 했듯, 결국 **Go 선호**의 문제임  
  - JS도 스크립트 언어로 나쁘지 않다고 생각함  
    `node bla.js`면 끝임  
  - 타입은 항상 신경 써야 함  
    함수가 뭘 반환하는지 알아야 하고, 언어를 잘 알면 기본 타입은 기억으로 처리함  
    이건 정적 타입 언어도 마찬가지임  
  - Python은 개발자에게는 훌륭하지만, **배포나 통합에는 악몽**임  
    다른 사람을 고려한다면 Python으로 배포용 코드를 쓰지 말아야 함

- 기대했던 건 Python 비판이었는데, 오히려 **유용한 팁**이었음  
  `//`를 주석으로 쓰는 언어라면 이 트릭을 응용할 수 있음  
  C/C++, Java, JavaScript, Rust, Swift, Kotlin, ObjC, D, F#, GLSL 등 가능함  
  특히 **GLSL**로 단일 파일 그래픽 데모를 만드는 게 흥미로움  
  [Shadertoy 예시](https://www.shadertoy.com/)  
  C에서는 블록 주석을 이용해 `/*/../usr/bin/env gcc "$0" "$@"; ./a.out; rm -vf a.out; exit; */` 같은 방식으로도 가능함
  - Swift에는 외부 의존성을 가진 스크립트를 실행할 수 있는 [swift-sh](https://github.com/xcode-actions/swift-sh) 프로젝트가 있음  
    Swift용 uv 같은 개념임  
    Swift는 shebang도 공식 지원함  
  - C/C++에서는 `#!`을 직접 써도 됨  
    예전 **TCC** 시절에 “C 스크립팅”으로 이런 방식을 썼음  
    큰 프로젝트에서는 빌드 스크립트가 매니페스트를 읽고 빌드 후 실행하는 구조였음  
    하지만 환경 제어가 어려워서 실무에는 부적합함  
  - Rust는 이런 꼼수를 쓸 필요 없음  
    shebang을 직접 지원함

- 더 **인체공학적인 언어**를 원한다면 .NET 10의 “run file directly” 기능도 있음  
  shebang을 지원하고, 스크립트 내에서 패키지를 자동 설치함  
  `#:sdk` 지시어로 웹앱도 바로 실행 가능함  
  - 나도 오늘 처음 이 기능으로 C# 스크립트를 작성했는데, 꽤 괜찮은 경험이었음  
    다만 **AOT 컴파일**은 아직 거칠음

- 처음엔 Python 비판일 줄 알았는데, 오히려 **언어 생태계의 방향성**을 생각하게 됨  
  ML이 Python에 묶인 건 큰 실수라고 봄  
  느리고, 타입 시스템이 불편하며, 배포가 어렵기 때문임  
  이제는 TypeScript, Go, Rust 같은 대안을 고려해야 함  
  - 동의함  
    다만 ML이 Python을 택한 이유는 **C 기반 FFI** 때문임  
    NodeJS나 Rust, Go는 FFI가 약함  
    Python은 여기서 강점을 가짐  
    이상적인 건 Python처럼 간단하지만 더 나은 타입 시스템과 배포 체계를 가진 언어임  
  - TypeScript로 대체하자는 건 동의 못함  
    JS 생태계에서 나온 언어로 Python을 대체하고 싶진 않음  
  - ML이 Python으로 간 건 시장 압력 때문임  
    Lisp이나 Lua(Torch)가 더 적합했지만, 단순함 때문에 Python이 선택됨  
    나도 Lisp 기반의 ML 프레임워크를 개발 중이지만, 채택은 어려울 것 같음  
  - Python의 **의존성 지옥**은 여전히 심각함  
    버전 호환성 문제, semver 부재, 불안정한 생태계 등으로 인해 JS보다 뒤처진 느낌임  
    JS/Node는 지난 10년간 성숙했지만 Python은 여전히 2012년에 머물러 있음  
    ML이 Python에 표준화된 건 정말 아쉬움  
  - 나는 단순하고 표현력 있으면서 **강타입 + 네이티브 컴파일**되는 언어를 원함  
    CLI 도구를 만들 때 Python보다 Go가 훨씬 빠름  
    LOC 차이 때문에 Python으로 돌아오긴 했지만, 실행할 때마다 Go가 그립음  
    아마 **OCaml**이 이상적이지만, 구식 툴링이 부담스러움

- Go 스크립트의 문제는 첫 줄에 공백이 없어야 한다는 점임  
  `gopls`가 자동 포맷팅을 강제하기 때문임  
  CI에서도 포맷 일관성을 유지해야 하므로, 이건 실무적으로 중요함  
  하지만 더 큰 문제는 **go.mod를 쓸 수 없다는 것**임  
  즉, 의존성 버전을 지정할 수 없어 호환성 보장이 약해짐  
  - 그래도 메이저 버전은 import 경로로 잠기기 때문에 기본적으로는 호환됨  
  - 이건 언어/런타임 수준의 호환성 문제이지, 의존성 문제는 아님
