Claude Skill

go-expert

Go 코드를 작성·리뷰·리팩터링하거나 에러 처리, 동시성, 테스트, net/http 서버, go:embed 를 다룰 때 사용한다. Go 1.22+ 기준.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download leeyudok-doksam-skills-skills_go-expert-841cccd.zip · 5 KB
Part of leeyudok/doksam-skills — 14 skills

Install

skills CLI npx skills add https://github.com/LeeYudok/doksam-skills/tree/main/skills/go-expert
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install leeyudok-doksam-skills@llmmart
Git git clone https://github.com/LeeYudok/doksam-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole leeyudok/doksam-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

go-expert

Go 코드가 대상이다. SQL·스키마는 sqlite-expert/db-expert, 프론트 산출물 내장의 빌드 순서는 frontend-build 가 맡는다.

이 문서는 일반론을 적지 않는다. 판단이 갈리는 지점, 자주 틀리는 곳, 최근 버전에서 바뀐 것만 담는다.

1. 에러

  • 감싸서 올린다. fmt.Errorf("%s 열기 실패: %w", path, err) — %w 여야 errors.Is/errors.As 가 통한다. %v 로 감싸면 사슬이 끊긴다.
  • 호출부가 분기해야 하는 실패는 센티널로 노출한다. var ErrNotFound = errors.New(...). 문자열 비교로 분기하지 않는다.
  • 메시지는 소문자로 시작하고 마침표를 붙이지 않는다. 한국어 메시지도 문장부호 없이 짧게.
  • 로그와 반환을 동시에 하지 않는다. 둘 다 하면 같은 실패가 여러 번 기록된다. 최상위(핸들러·main)에서 한 번만 기록한다.
  • panic 은 프로그래머 오류에만. 입력이 잘못된 것은 에러다.
if errors.Is(err, chatdb.ErrNotFound) { ... }   // 분기
var perr *fs.PathError
if errors.As(err, &perr) { ... }                 // 타입 정보가 필요할 때

2. 동시성 — 필요할 때만

goroutine 을 띄우기 전에 답한다: 누가 이걸 멈추는가? 결과는 누가 받는가? 답이 없으면 만들지 않는다.

  • goroutine 의 수명은 호출부가 통제한다. context.Context 를 첫 인자로 받고, 종료 신호를 존중한다. 구조체 필드에 context 를 넣지 않는다.
  • 채널로 소유권을 옮기거나, 뮤텍스로 공유를 보호하거나 — 둘을 섞지 않는다.
  • sync.WaitGroup 은 Add 를 goroutine 밖에서 부른다. 안에서 부르면 경합이다.
  • 루프 변수 캡처는 Go 1.22부터 반복마다 새 변수라 안전하다. 그 이전 버전 코드를 손볼 때는 여전히 확인한다.
  • 테스트는 -race 로 돌린다. 동시성 코드를 추가·수정했으면 필수다.

3. net/http — Go 1.22+ ServeMux

메서드와 경로 변수를 표준 mux 가 지원한다. 서드파티 라우터를 새로 들이기 전에 이걸로 충분한지 본다.

mux.HandleFunc("GET /api/chat/refs/{ref}/rooms", h)
mux.HandleFunc("DELETE /api/chat/dbs/{db}", h)
// 핸들러에서
ref := r.PathValue("ref")
  • 더 구체적인 패턴이 우선한다 — /api/... 를 등록해두면 / 폴백이 삼키지 않는다.
  • 경로 변수는 디코딩된 값이다. 파일명·경로로 쓸 거면 반드시 검증한다(§5).
  • 서버에는 최소한 ReadHeaderTimeout 을 준다. 없으면 느린 헤더 공격에 매달린다.
  • 미들웨어는 핸들러를 감싸는 함수로. 인가처럼 빠뜨리면 안 되는 것은 라우팅 등록 지점에서 한 번에 걸리게 만든다 — 핸들러 안에서 각자 검사하면 언젠가 빠진다.
mux.HandleFunc("GET /api/x", requireAdmin(cfg, "X", handleX))

4. go:embed

  • //go:embed 는 같은 디렉터리 이하만 가리킨다. ../ 로 못 올라간다. 상위 폴더의 산출물을 넣으려면 그 폴더 안에 embed 하는 패키지를 둔다.
  • 기본 패턴은 .·_ 로 시작하는 파일을 건너뛴다. 포함하려면 all: 접두사.
  • 패턴이 하나도 안 맞으면 컴파일 에러다. 산출물을 커밋하지 않는 구조라면 자리표시자를 하나 커밋하고 //go:embed all:dist 로 받는다.
  • 내장 여부를 런타임에 확인해 안내를 띄운다. 빈 화면보다 원인 추적이 훨씬 빠르다.
//go:embed all:dist
var dist embed.FS

func Built() bool { _, err := fs.Stat(Assets(), "index.html"); return err == nil }

SPA 를 서빙한다면 정적 파일에 없는 경로는 404 가 아니라 index.html 을 돌려줘야 새로고침·직접 접속이 동작한다.

5. 입력 검증

  • 파일명은 filepath.Base 로 정규화하고, 남은 값에 구분자가 있으면 거절한다. 경로 조작은 여기서 끊는다.
  • 경로를 받는 기능은 절대 경로 여부·확장자·심볼릭 링크를 푼 뒤(filepath.EvalSymlinks) 허용 범위를 검사한다. 링크는 검사 우회의 표준 수법이다.
  • 업로드는 io.LimitReader 로 상한을 두고, 넘으면 지운다. 상한이 없으면 디스크를 채우는 것만으로 서비스가 멈춘다.
  • 형식만 맞고 내용이 아닌 파일을 걸러내려면 실제로 열어서 판정한다.

6. 테스트

  • 테이블 주도가 기본. 케이스마다 이름을 주고 실패 메시지에 넣는다.
  • 실패 메시지는 got = X, 원하는 값 Y 형태로 무엇이 왜 틀렸는지 드러낸다.
  • t.TempDir()·t.Chdir()·t.Cleanup() 을 쓴다. 수동 정리는 실패 경로에서 새어나간다.
  • HTTP 는 httptest.NewServer + 실제 mux 로 인가까지 함께 검증한다. 핸들러 함수만 직접 부르면 미들웨어가 빠져 "권한 없이도 되는" 회귀를 못 잡는다.
  • 보안 요건은 반드시 테스트로 고정한다 — 권한 없는 접근이 403 인지, 주입 시도가 거부되는지, 민감 자산이 다른 API 로 새지 않는지. 문서에만 적힌 규칙은 다음 리팩터링에서 사라진다.
  • 외부 자원이 필요한 통합 테스트는 환경변수로 opt-in 하고 없으면 t.Skip.
if got != want {
    t.Errorf("%s: 방 수 = %d, 원하는 값 %d", tc.name, got, want)
}

7. API 표면

  • 반환 타입은 호출부가 다뤄야 할 만큼만 노출한다. 내부 구조체를 그대로 JSON 으로 내보내지 않는다 — 필드가 늘면 조용히 새어나간다. DTO 를 따로 둔다.
  • 슬라이스를 돌려줄 때 빈 결과는 nil 대신 빈 슬라이스로. JSON 에서 null 과 [] 는 다르다.
  • 인터페이스는 쓰는 쪽에서 정의한다. 구현 쪽에 미리 만들어두지 않는다.
  • 문서 주석은 이름으로 시작한다: // Search 는 ....

8. 완료 조건

  • gofmt -l 출력 없음, go vet ./... 통과
  • go test ./... 통과 (동시성 변경이 있으면 -race 포함)
  • 새 엔드포인트·기능에 인가·검증 테스트가 있음
  • 에러가 %w 로 감싸져 있고, 분기 대상은 센티널로 노출됨
  • 외부 입력(파일명·경로·ID)이 검증 후에만 쓰임
Files (doksam-skills)
  • agents
    • antigravity.md 534 B
      ---
      name: go-expert
      description: Go 코드를 작성·리뷰·리팩터링하거나 에러 처리, 동시성, 테스트, net/http 서버, go:embed 를 다룰 때 사용한다. Go 1.22+ 기준.
      ---
      
      # go-expert
      
      Go 코드를 작성·리뷰·리팩터링하거나 에러 처리, 동시성, 테스트, net/http 서버, go:embed 를 다룰 때 사용한다. Go 1.22+ 기준.
      
      `go-expert` Skill 을 작업 계약의 단일 원본으로 사용한다. Antigravity Managed Agent
      등록 시 이 파일의 내용을 역할 정의로 넣는다.
      
    • claude.md 403 B
      ---
      name: go-expert
      description: Go 코드를 작성·리뷰·리팩터링하거나 에러 처리, 동시성, 테스트, net/http 서버, go:embed 를 다룰 때 사용한다. Go 1.22+ 기준.
      skills:
        - go-expert
      ---
      
      `go-expert` Skill 을 작업 계약의 단일 원본으로 사용한다.
      
      역할·절차·산출물 형식은 Skill 에 있는 것을 따르고, 이 파일에 복제하지 않는다.
      
    • codex.toml 368 B
      name = "go_expert"
      description = "Go 코드를 작성·리뷰·리팩터링하거나 에러 처리, 동시성, 테스트, net/http 서버, go:embed 를 다룰 때 사용한다. Go 1.22+ 기준."
      developer_instructions = """
      Use the go-expert skill as the single source of truth for the task.
      Follow its workflow and deliverable contract; do not restate them here.
      """
      
    • openai.yaml 238 B
      interface:
        display_name: "Go Expert"
        short_description: "Go 관용구·에러 처리·동시성·net/http·테스트"
        default_prompt: "$go-expert 로 이 Go 코드를 리뷰하고 에러 처리와 동시성 관점에서 고쳐줘."
      
  • SKILL.md 6.4 KB
    ---
    name: go-expert
    description: Go 코드를 작성·리뷰·리팩터링하거나 에러 처리, 동시성, 테스트, net/http 서버, go:embed 를 다룰 때 사용한다. Go 1.22+ 기준.
    ---
    
    # go-expert
    
    Go 코드가 대상이다. SQL·스키마는 `sqlite-expert`/`db-expert`,
    프론트 산출물 내장의 빌드 순서는 `frontend-build` 가 맡는다.
    
    이 문서는 **일반론을 적지 않는다.** 판단이 갈리는 지점, 자주 틀리는 곳, 최근 버전에서
    바뀐 것만 담는다.
    
    ## 1. 에러
    
    - **감싸서 올린다.** `fmt.Errorf("%s 열기 실패: %w", path, err)` — `%w` 여야
      `errors.Is`/`errors.As` 가 통한다. `%v` 로 감싸면 사슬이 끊긴다.
    - **호출부가 분기해야 하는 실패는 센티널로 노출한다.** `var ErrNotFound = errors.New(...)`.
      문자열 비교로 분기하지 않는다.
    - 메시지는 소문자로 시작하고 마침표를 붙이지 않는다. 한국어 메시지도 문장부호 없이 짧게.
    - **로그와 반환을 동시에 하지 않는다.** 둘 다 하면 같은 실패가 여러 번 기록된다.
      최상위(핸들러·main)에서 한 번만 기록한다.
    - `panic` 은 프로그래머 오류에만. 입력이 잘못된 것은 에러다.
    
    ```go
    if errors.Is(err, chatdb.ErrNotFound) { ... }   // 분기
    var perr *fs.PathError
    if errors.As(err, &perr) { ... }                 // 타입 정보가 필요할 때
    ```
    
    ## 2. 동시성 — 필요할 때만
    
    goroutine 을 띄우기 전에 답한다: **누가 이걸 멈추는가? 결과는 누가 받는가?**
    답이 없으면 만들지 않는다.
    
    - **goroutine 의 수명은 호출부가 통제한다.** `context.Context` 를 첫 인자로 받고,
      종료 신호를 존중한다. 구조체 필드에 context 를 넣지 않는다.
    - 채널로 소유권을 옮기거나, 뮤텍스로 공유를 보호하거나 — **둘을 섞지 않는다.**
    - `sync.WaitGroup` 은 `Add` 를 goroutine **밖에서** 부른다. 안에서 부르면 경합이다.
    - 루프 변수 캡처는 Go 1.22부터 반복마다 새 변수라 안전하다. **그 이전 버전 코드를
      손볼 때는** 여전히 확인한다.
    - **테스트는 `-race` 로 돌린다.** 동시성 코드를 추가·수정했으면 필수다.
    
    ## 3. net/http — Go 1.22+ ServeMux
    
    메서드와 경로 변수를 표준 mux 가 지원한다. 서드파티 라우터를 새로 들이기 전에 이걸로
    충분한지 본다.
    
    ```go
    mux.HandleFunc("GET /api/chat/refs/{ref}/rooms", h)
    mux.HandleFunc("DELETE /api/chat/dbs/{db}", h)
    // 핸들러에서
    ref := r.PathValue("ref")
    ```
    
    - 더 구체적인 패턴이 우선한다 — `/api/...` 를 등록해두면 `/` 폴백이 삼키지 않는다.
    - **경로 변수는 디코딩된 값**이다. 파일명·경로로 쓸 거면 반드시 검증한다(§5).
    - 서버에는 최소한 `ReadHeaderTimeout` 을 준다. 없으면 느린 헤더 공격에 매달린다.
    - 미들웨어는 핸들러를 감싸는 함수로. 인가처럼 빠뜨리면 안 되는 것은
      **라우팅 등록 지점에서 한 번에 걸리게** 만든다 — 핸들러 안에서 각자 검사하면 언젠가 빠진다.
    
    ```go
    mux.HandleFunc("GET /api/x", requireAdmin(cfg, "X", handleX))
    ```
    
    ## 4. go:embed
    
    - `//go:embed` 는 **같은 디렉터리 이하만** 가리킨다. `../` 로 못 올라간다.
      상위 폴더의 산출물을 넣으려면 **그 폴더 안에 embed 하는 패키지를 둔다.**
    - 기본 패턴은 `.`·`_` 로 시작하는 파일을 건너뛴다. 포함하려면 `all:` 접두사.
    - **패턴이 하나도 안 맞으면 컴파일 에러다.** 산출물을 커밋하지 않는 구조라면
      자리표시자를 하나 커밋하고 `//go:embed all:dist` 로 받는다.
    - 내장 여부를 런타임에 확인해 안내를 띄운다. 빈 화면보다 원인 추적이 훨씬 빠르다.
    
    ```go
    //go:embed all:dist
    var dist embed.FS
    
    func Built() bool { _, err := fs.Stat(Assets(), "index.html"); return err == nil }
    ```
    
    SPA 를 서빙한다면 **정적 파일에 없는 경로는 404 가 아니라 index.html** 을 돌려줘야
    새로고침·직접 접속이 동작한다.
    
    ## 5. 입력 검증
    
    - **파일명은 `filepath.Base` 로 정규화**하고, 남은 값에 구분자가 있으면 거절한다.
      경로 조작은 여기서 끊는다.
    - 경로를 받는 기능은 절대 경로 여부·확장자·**심볼릭 링크를 푼 뒤**(`filepath.EvalSymlinks`)
      허용 범위를 검사한다. 링크는 검사 우회의 표준 수법이다.
    - 업로드는 `io.LimitReader` 로 상한을 두고, 넘으면 지운다. 상한이 없으면 디스크를 채우는
      것만으로 서비스가 멈춘다.
    - 형식만 맞고 내용이 아닌 파일을 걸러내려면 **실제로 열어서 판정**한다.
    
    ## 6. 테스트
    
    - **테이블 주도**가 기본. 케이스마다 이름을 주고 실패 메시지에 넣는다.
    - 실패 메시지는 `got = X, 원하는 값 Y` 형태로 **무엇이 왜 틀렸는지** 드러낸다.
    - `t.TempDir()`·`t.Chdir()`·`t.Cleanup()` 을 쓴다. 수동 정리는 실패 경로에서 새어나간다.
    - HTTP 는 `httptest.NewServer` + 실제 mux 로 **인가까지 함께** 검증한다.
      핸들러 함수만 직접 부르면 미들웨어가 빠져 "권한 없이도 되는" 회귀를 못 잡는다.
    - **보안 요건은 반드시 테스트로 고정한다** — 권한 없는 접근이 403 인지, 주입 시도가 거부되는지,
      민감 자산이 다른 API 로 새지 않는지. 문서에만 적힌 규칙은 다음 리팩터링에서 사라진다.
    - 외부 자원이 필요한 통합 테스트는 환경변수로 opt-in 하고 없으면 `t.Skip`.
    
    ```go
    if got != want {
        t.Errorf("%s: 방 수 = %d, 원하는 값 %d", tc.name, got, want)
    }
    ```
    
    ## 7. API 표면
    
    - 반환 타입은 **호출부가 다뤄야 할 만큼만** 노출한다. 내부 구조체를 그대로 JSON 으로
      내보내지 않는다 — 필드가 늘면 조용히 새어나간다. DTO 를 따로 둔다.
    - 슬라이스를 돌려줄 때 빈 결과는 `nil` 대신 빈 슬라이스로. JSON 에서 `null` 과 `[]` 는 다르다.
    - 인터페이스는 **쓰는 쪽에서** 정의한다. 구현 쪽에 미리 만들어두지 않는다.
    - 문서 주석은 이름으로 시작한다: `// Search 는 ...`.
    
    ## 8. 완료 조건
    
    - `gofmt -l` 출력 없음, `go vet ./...` 통과
    - `go test ./...` 통과 (동시성 변경이 있으면 `-race` 포함)
    - 새 엔드포인트·기능에 인가·검증 테스트가 있음
    - 에러가 `%w` 로 감싸져 있고, 분기 대상은 센티널로 노출됨
    - 외부 입력(파일명·경로·ID)이 검증 후에만 쓰임
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related