Community코딩 & 개발github.com

davidvella/go-style

읽기 쉽고 빠른 Go 코드를 작성, 검토 또는 리팩터링합니다. 함수를 작게 유지하고 순환 복잡도와 인지 복잡도를 낮추며, 영리한 코드보다 API 설계와 데이터 레이아웃에 성능을 담고, 벤치마크, 할당량, 이스케이프 분석으로 변경 사항을 증명합니다.

go-style란 무엇인가요?

이 스킬은 Go 언어 개발자가 유지보수성과 성능을 동시에 만족하는 코드를 작성하도록 돕습니다. 주요 원칙은 '읽기 쉽고 빠른 Go 스타일'로, 함수를 작고 단순하게 유지하며, 성능을 코드 내 트릭이 아닌 API 설계와 데이터 구조에서 얻도록 유도합니다. Dave Cheney의 'API 설계를 성능 프리미티브로' 접근법을 따르며, 핫 경로와 콜드 경로를 명확히 분리하고, 벤치마크와 할당 분석을 통해 성능 주장을 증명하도록 요구합니다. 또한 함수는 순환 복잡도 10 이하, 인지 복잡도 10 이하를 목표로 하며, 뷰 반환, 호출자 소유 메모리, 오류를 핫 루프 밖으로 정의하는 등의 구체적인 패턴을 제시합니다. 이 스킬은 리뷰와 리팩터링 과정에서도 동일한 기준을 적용하여, 변경 전후의 벤치마크와 할당량을 비교하고 복잡도를 측정하도록 합니다. 주로 Cursor, Claude Code, Codex와 같은 AI 에이전트 환경에서 Go 프로젝트의 코드 품질과 성능을 개선하는 데 사용됩니다.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/davidvella/skills/tree/HEAD/skills/go-style

Installed? Explore more 코딩 & 개발 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

Go 스타일: 읽기 쉽고 빠르게

목표는 유지보수자가 코드를 위에서 아래로 읽을 수 있으면서, 성능은 코드 전체에 흩어진 트릭이 아니라 API와 데이터의 형태에서 나오게 하는 것입니다. 설계가 올바르면 가독성과 속도는 거의 충돌하지 않습니다. 충돌할 경우, 측정된 핫 경로에서만 가독성을 포기하고, 그 이유를 주석에 명시하십시오.

이 가이드는 Dave Cheney의 "API 설계를 성능 프리미티브로"를 따릅니다. 일정 수준 이상에서는 성능을 나중에 추가할 수 없으므로, 처음부터 설계하고 나머지는 단순하게 유지하십시오.

출처: Cheney의 발표(https://dave.cheney.net/paste/gophercon-uk-2026.html), github.com/pkg/json 및 github.com/davecheney/bytereader의 코드; John Ousterhout, "Define errors out of existence".

우선순위

  1. 정확성. 속도나 간결함을 위해 절대 희생하지 마십시오.
  2. API 형태와 big-O. 누가 메모리를 소유하는지, 무엇이 반환되는지, 각 호출이 얼마나 많은 작업을 하는지. 이것이 이후의 어떤 튜닝보다 성능을 결정합니다.
  3. 핫 경로에서의 할당. 각 할당은 할당 한 번, 가비지 컬렉터 한 번, 총 두 번의 비용이 듭니다. 컬렉터가 빠를 수는 있지만 "할당을 하지 않는 것이 항상 더 빠릅니다."
  4. 입력 단위당 작업. 각 바이트를 한 번만 건드리고, 바이트당이 아니라 토큰 또는 레코드당 호출을 만드십시오.
  5. 마이크로 최적화(수동 인라이닝, 비트 트릭, 경계 검사 회피)는 프로파일이 핫하다고 보여주는 경로에서만, 그리고 이득을 증명하는 벤치마크와 함께만 사용하십시오.

변경 사항이 위 수준에서 정당화되지 않으면, 수행하지 마십시오.

핫 경로와 콜드 경로 분리

어떤 코드가 이벤트나 요청당 실행되는지(핫)와 설정, 빌드, 구성 또는 업데이트 시간에 실행되는지(콜드)를 결정하십시오.

  • 콜드 경로: 가장 단순한 코드를 작성하십시오. 공유 대신 복제하고, 맵과 fmt를 사용하며, 자유롭게 할당하십시오. 핫 경로 관용구를 여기에 가져오지 마십시오. 가독성만 해치고 얻는 것이 없습니다.
  • 핫 경로: 재사용 가능한 버퍼, 세대 카운터, 평면 배열 및 유사한 기술을 사용할 수 있습니다. 각 기술에 이름(작은 타입이나 헬퍼)을 붙이고 왜 존재하는지 설명하는 주석을 추가하여, 독자가 역공학하지 않도록 하십시오.

성능을 위한 API 설계

API는 호출자가 절약할 수 있는 최대치를 결정합니다. API가 모든 결과를 힙에 강제로 넣으면 "가비지 컬렉터를 벤치마킹하는" 꼴이 됩니다. 빠른 경로가 자연스러운 경로가 되도록 형태를 만드십시오. 2-10배의 이득은 종종 "다른 API를 받아들일 준비가 되어 있다면" 얻을 수 있습니다.

  • 이스케이프해야 하는 값을 반환하지 마십시오. interface{}/any, 새 문자열, 맵 또는 항목당 새 구조체에 대한 포인터를 통해 프리미티브를 반환하면 호출당 할당이 강제됩니다(encoding/json.Decoder.Token이 전형적인 "가비지 공장"입니다). 구체적인 타입이나 []byte와 같은 뷰를 반환하고, 호출자가 필요한 것만 변환하도록 하십시오.
  • 뷰를 반환하고, 복사본을 반환하지 마십시오. 호출자가 읽기만 하면 되는 경우에 적용하십시오. 뷰가 유효한 기간을 문서화하십시오("다음 호출까지 유효", bufio.Scanner.Bytes처럼). 원시 입력의 뷰(첫 번째 바이트가 종류를 알려주는 토큰의 바이트)는 호출자가 무시할 수 있는 디코딩된 값보다 저렴합니다.
  • 호출자가 재사용 가능한 메모리를 소유하도록 하십시오. 호출당 할당하는 대신 스크래치나 버퍼 인수를 받거나 호출자가 유지하는 구조체에 보관하십시오. 호출자가 초기 버퍼를 제공할 수 있게 하여 스택에 있거나 풀에서 올 수 있도록 하십시오: var buf [8 << 10]byte; d := NewDecoderBuffer(r, buf[:]).
  • 버퍼 관리를 몇 가지 메서드 뒤에 숨기십시오. io.Reader는 호출자가 관리하는 버퍼로 복사하고 모든 호출자가 다시 채우도록 합니다. 작은 윈도우 API는 데이터를 제자리에 유지합니다: Window() []byte로 보기, Release(n)으로 소비, Extend() int로 더 읽기, 문서 주석에 "윈도우는 Release 또는 Extend에 의해 무효화됩니다"라고 명시.
  • 오류를 핫 루프 밖으로 정의하십시오. bool이나 빈 결과를 반환하고 오류를 나중에 보고하는 반복을 사용하십시오: for s.Next() { ... } 그 다음 s.Err(). 느린 경로에 센티널 결과를 사용하십시오(Extend가 0을 반환하면 호출자가 오류를 물음). 내부 루프에 오류 분기가 없도록 하십시오. io.EOF를 다른 종료 조건처럼 취급하십시오.
  • 입력 단위당 작업을 계산하십시오. 각 바이트를 한 번 처리하고, 바이트당이 아니라 토큰이나 레코드당 함수 호출을 만드십시오. 중간 복사본을 제거하십시오. 바이트당 호출에서 토큰당 호출로 이동하는 것은 종종 할당 제거 다음으로 두 번째로 큰 이득입니다.
  • API를 계층화하십시오. 저수준의 할당이 없는 빌딩 블록(원시 토큰, 뷰, 스크래치를 소유하는 반복자)을 제공하고 그 위에 편리한 API를 구축하십시오. 편리하고 할당하는 API만 유일한 API로 만들지 마십시오.
  • 소유권과 수명을 문서 주석에 작성하십시오: 무엇이 빌려오는지, 무엇이 유지되는지, 동시 사용에 안전한지, 제로 값이 무엇을 의미하는지.
  • 제로 값을 유용하게 만드십시오. 자연스러운 경우에만 적용하십시오.
  • 변경 가능한 공유 상태에 대한 잠금보다 불변 공유 값과 새 스냅샷 게시를 선호하십시오. 읽기 작업이 지배적인 경우.

구현뿐만 아니라 API를 측정하십시오: 교체하는 API와 비교하여 allocs/op와 처리량을 비교하십시오. references/examples.md는 이러한 형태를 나란히 보여줍니다.

핫 루프 튜닝, 마지막으로

API와 big-O가 올바르고 프로파일이 루프가 핫하다고 보여준 후에만:

  • 상태에 대한 switch는 비교 체인으로 컴파일됩니다. func 필드(d.state = (*Decoder).stateValue)를 통해 선택된 상태별 작은 메서드는 각 상태를 읽기 쉽게 유지하고 디스패치를 제거할 수 있습니다.
  • 검사를 값을 사용하는 함수로 이동하여 컴파일러가 경계 검사가 불필요함을 증명할 수 있게 하고, 엔트리 포인트를 인라이닝 예산(80; 호출 비용은 약 52) 내에 유지하십시오. go build -gcflags=-m=2로 확인하십시오.
  • 호출을 피하기 위해 두 함수를 병합하면 토큰당 경로에서 5-15%의 이득을 얻을 수 있지만, 더 깊은 중첩의 대가가 따릅니다. 벤치마크와 함께만 수행하고 이유를 주석에 명시하십시오.

Cheney가 말했듯이: "API의 big O 효과를 다루지 않고" 이러한 트릭을 사용하지 마십시오. "바늘을 움직이지 않을 것입니다."

함수를 작고 평평하게 유지

각 함수가 하나의 일을 하고, 기껏해야 한두 개의 분기를 갖도록 하십시오. 지침으로, 순환 복잡도를 10 이하, 인지 복잡도를 10 이하로 유지하십시오(gocyclo, gocognit). 각 케이스를 하나의 호출로 넘기는 평면 타입 스위치는 그 이상도 괜찮지만, 중첩은 안 됩니다.

  • 다중 조건에 이름을 붙이십시오. if len(a)*len(b) > maxN || size(a)*len(b)+size(b)*len(a) > maxM를 if !fitsProduct(a, b)로 바꾸십시오. 이름이 의도를 문서화하고 함수가 설명을 담을 수 있습니다.
  • 디스패치 후 위임하십시오. 노드나 메시지 종류에 대한 스위치는 케이스당 하나의 메서드를 호출해야 하지, 각 케이스의 로직을 포함해서는 안 됩니다.
  • 일찍 반환하십시오. 가드 절을 먼저, 주요 경로는 들여쓰기 없이.
  • 케이스를 반복하지 마십시오. 두 분기가 피연산자 순서만 다르다면, 하나의 헬퍼를 작성하고 두 번 호출하십시오.
  • 혼합된 관심사를 분리하십시오. "찾기", "새 값 만들기", "정리하기"는 하나가 아닌 세 개의 함수입니다.
  • bool 플래그 매개변수를 피하십시오. 가장 명확한 옵션이라면, 의미 있는 이름을 붙이고 호출 지점에서 설명하십시오.
  • 룩업 테이블(map[string]Kind, [256]bool)을 문자열이나 바이트 비교 체인 대신 사용하십시오.

독자와 CPU를 위한 데이터 형태 만들기

  • 병렬 슬라이스를 구조체 슬라이스로 그룹화하십시오. 동일한 ID로 인덱싱된 네 개의 슬라이스는 하나의 구조체입니다. 읽기에 좋고 일반적으로 지역성을 향상시킵니다.
  • 반복되는 메커니즘에 자체 작은 타입을 부여하십시오. 동일한 카운터 랩, 비트맵 또는 버퍼 로직이 두 번 나타나면, 두세 개의 메서드를 가진 타입입니다.
  • 비트 패킹보다 명시적 필드를 선호하십시오. 패킹이 측정된 결과에 영향을 미치는 경우가 아니라면. []segment{name, each}가 시프트 연산이 있는 uint64 플래그보다 낫습니다.
  • 표현을 적게 유지하십시오. 상태나 종류가 적을수록 중복된 스위치가 줄어듭니다. 측정 가능한 비용이 없다면 특수 케이스를 일반 케이스에 병합하십시오.

성능 주장 증명

측정 없이 변경이 더 빠르다, 느리다, 또는 할당이 없다고 주장하지 마십시오. 성능에 민감한 코드를 변경하기 전:

  1. 관련 벤치마크의 benchstat 기준선을 저장하십시오.
  2. 변경을 수행한 후 비교하십시오. 유의미한 차이를 숫자로 보고하십시오.
  3. go build -gcflags=-m 출력을 기준선과 비교하여 새로운 힙 이스케이프를 확인하십시오.
  4. 제로 할당을 약속하는 경로에 대해 testing.AllocsPerRun 테스트를 유지하거나 추가하십시오.
  5. 회귀 원인을 추측하기 전에 프로파일링하십시오.

단순화가 측정 가능한 핫 경로 시간을 소모하고 이를 깔끔하게 회복할 수 없다면, 멈추고 트레이드오프를 보고하십시오. 조용히 복잡성을 다시 추가하거나 조용히 회귀를 받아들이지 마십시오.

정확한 명령과 복잡도 측정 방법 및 일반적인 회귀 원인에 대해서는 references/verify.md를 읽으십시오. 스타일을 보여주는 전후 리팩터 예제는 references/examples.md를 읽으십시오.

Go 관례 따르기

  • gofmt, go vet 및 프로젝트의 린터를 통과해야 합니다. 코드를 수정하고, 린터 규칙을 약화시키거나 제외하지 마십시오. 명명된 규칙과 이유가 있을 때만 한 줄을 억제하십시오.
  • 짧은 리시버 이름, 타입의 메서드 전체에서 일관되게 사용하십시오. 이유 없이 하나의 타입에 포인터와 값 리시버를 혼용하지 마십시오.
  • 문서 주석은 식별자 이름으로 시작하고 계약을 명시하십시오.
  • 무엇이 아니라 왜를 주석으로 남기십시오. 주변 주석 밀도와 일치시키십시오.
  • 직접 작성한 헬퍼보다 표준 라이브러리(slices, maps, strings, math/bits)를 우선 사용하십시오.
  • 생성된 코드를 직접 편집하지 마십시오.

리뷰 또는 리팩터링 시

  1. 아무것도 건드리기 전에 핫 경로와 그 벤치마크를 식별하십시오.
  2. 복잡도를 측정하여 최악의 함수를 찾고, 그 함수부터 먼저 수정하십시오.
  3. 작은 단계로 리팩터링하고, 각 단계 후에 집중된 테스트를 실행하십시오.
  4. 동작을 동일하게 유지하십시오: 동일한 결과, 동일한 예산 및 폴백, 동일한 할당 보장. 동작 변경은 별도의 명시된 결정으로만 수행하십시오.
  5. 무엇이 변경되었는지, 전후 복잡도와 벤치마크 수치, 그리고 검증되지 않은 모든 것을 보고하십시오.

관련 스킬