Go 언어에서 nil 포인터로 인한 오류를 미리 잡는 방법: 정적 분석 도구 활용법
Go 언어에서 nil 포인터로 인한 오류를 정적 분석으로 잡아내는 방법
Go 언어를 사용하다 보면 nil
로 인한 오류는 자주 접하는 문제 중 하나인데요.
특히 nil 포인터
와 nil 인터페이스
의 차이로 인해 발생하는 문제는 디버깅하기 쉽지 않습니다.
이번 글에서는 정적 분석을 통해 이러한 오류를 미리 찾아내는 방법을 소개하려고 합니다.
이를 통해 코드에서 발생할 수 있는 잠재적인 문제를 미리 예방할 수 있겠죠.
문제 상황: nil인데 nil이 아닌 이유
먼저 아래 코드를 실행해보세요.
package main
import "fmt"
type MyErr struct{}
func (*MyErr) Error() string {
return "MyErr"
}
func F1() *MyErr {
return nil
}
func F2() error {
return F1()
}
func main() {
err := F2()
if err != nil {
fmt.Println("Error!")
}
}
위 코드를 보면 F1()
함수는 nil
을 반환하고, 따라서 F2()
도 nil
을 반환할 것처럼 보이는데요.
하지만 err
는 nil
이 아니고 "Error!"가 출력됩니다. 왜 이런 일이 일어나는 걸까요?
이 문제는 Go 언어에서 nil 포인터를 인터페이스에 넣을 때 발생하는 특성 때문입니다.
Go에서는 명시적인 형 변환이 필요하지만, 인터페이스 타입으로 변환할 때는 암묵적으로 변환이 이루어집니다.
위 코드에서 F1()
의 반환값은 *MyErr
타입이지만, F2()
에서 반환할 때는 error
타입(인터페이스)으로 암묵적으로 변환됩니다.
Go의 nil
리터럴은 포인터로서의 nil과 인터페이스로서의 nil 두 가지 의미를 가집니다.
인터페이스로서의 error
타입은 nil
과 비교하여 에러 여부를 판단하는데, 포인터로서 nil
을 넣으면 이것이 nil
이 아님에도 불구하고 에러로 처리되는 경우가 발생할 수 있습니다.
이러한 문제는 컴파일 단계에서는 걸러지지 않기 때문에, 코드 상에서 쉽게 눈에 띄지도 않습니다.
그래서 정적 분석을 통해 이러한 오류를 미리 탐지하는 것이 중요합니다.
정적 분석 도구 만들기
이 문제를 해결하기 위해 직접 정적 분석 도구를 만들어 보았습니다.
이 도구는 Go의 정적 분석 도구(skeleton)를 사용해 구현했는데요, 먼저 코드에서 포인터가 error 타입에 할당되는 위치를 찾아내는 것이 목표입니다.
대상 코드
코드에서 포인터가 error 타입에 할당되는 경우는 다음과 같은 경우입니다:
- 변수에 할당
return
문- 함수 인자
이 중에서 이번에는 변수 할당과 return 문을 분석 대상으로 삼았습니다.
에러 타입에 포인터가 할당된 경우 탐지
먼저 변수 할당을 탐지하는 방법을 알아볼까요? Go 언어에서는 여러 변수를 한 번에 할당할 수 있으므로, 좌변(Lhs)과 우변(Rhs)을 따로 확인해야 합니다.
좌변이 error
타입이고, 우변이 포인터 타입인지 확인하는 것이 핵심입니다.
func checkAssign(pass *analysis.Pass, n *ast.AssignStmt) {
for i := range n.Lhs {
lt := pass.TypesInfo.TypeOf(n.Lhs[i])
rt := pass.TypesInfo.TypeOf(n.Rhs[i])
_, rtIsPtr := rt.(*types.Pointer)
if lt == errType && rtIsPtr {
pass.Reportf(n.Pos(), "Assign pointer to error")
}
}
}
위 코드에서 TypeOf()
를 사용해 변수의 타입을 확인하고, 좌변이 error
타입인지, 우변이 포인터 타입인지를 체크해줍니다.
이 조건이 맞으면 오류를 보고합니다.
포인터를 반환하는 경우 탐지
다음으로 return 문에서 포인터를 반환하는 경우를 탐지하는 방법을 살펴봅시다.
여기서는 함수 정의에서 반환 타입을 확인하고, 함수 본문에서 return 문을 찾아서 포인터가 반환되는지를 확인합니다.
func checkFuncReturn(pass *analysis.Pass, t *ast.FuncType, b *ast.BlockStmt) {
if t.Results == nil {
return
}
var idxs []int
for i, r := range t.Results.List {
if pass.TypesInfo.TypeOf(r.Type) == errType {
idxs = append(idxs, i)
}
}
if len(idxs) == 0 {
return
}
ast.Inspect(b, func(n ast.Node) bool {
switch n := n.(type) {
case *ast.FuncLit:
return false
case *ast.ReturnStmt:
for _, i := range idxs {
_, isPtr := pass.TypesInfo.TypeOf(n.Results[i]).(*types.Pointer)
if isPtr {
pass.Reportf(n.Pos(), "Return pointer as error")
}
}
}
return true
})
}
위 코드에서는 함수의 반환 타입을 먼저 확인하고, 그 중에서 error
타입인 반환값이 있는지 체크합니다.
그런 다음, 그 반환값이 포인터 타입인지 확인하여, 포인터가 error
로 반환되는 경우를 보고합니다.
결과 확인하기
이제 위에서 만든 도구를 사용해 처음 제시한 코드를 분석해보겠습니다. 다음 명령어를 실행해보면:
$ ptrtoerr example.go
./example.go:16:2: Return pointer as error
F2()
함수의 return
문에서 포인터가 error
타입으로 반환된다는 것을 정확하게 탐지할 수 있습니다.
마무리
이번 글에서는 Go 언어에서 nil 포인터
로 인해 발생할 수 있는 오류를 정적 분석을 통해 어떻게 미리 탐지할 수 있는지 알아보았습니다.
Go의 정적 분석 도구를 사용하면, 이러한 오류를 컴파일 전에 쉽게 찾아낼 수 있죠.
처음 정적 분석을 시도해본 결과, 예상보다 간단하게 구현할 수 있었고, 여러분에게도 큰 도움이 되길 바랍니다!
'Go' 카테고리의 다른 글
Go로 작성된 로컬 파일 처리 CLI 도구 테스트 방법 3가지 (0) | 2024.09.20 |
---|---|
Go 언어로 HTTP 서버 기본 구조 깔끔하게 잡기: `errgroup` 활용! (0) | 2024.09.20 |
Go 언어에서 Templ 또는 일반 템플릿: 무엇을 선택해야 할까요? (0) | 2024.09.19 |
Go 언어에서 구조체 필드를 반드시 지정하여 초기화하는 방법 알아볼까요? (0) | 2024.09.19 |
Go 언어 time.Timer#Reset() 완벽 가이드: 올바른 사용법 알아볼까요? (0) | 2024.09.19 |