ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Go 언어에서 nil 포인터로 인한 오류를 미리 잡는 방법: 정적 분석 도구 활용법
    Go 2024. 9. 20. 11:07

    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을 반환할 것처럼 보이는데요.

     

    하지만 errnil이 아니고 "Error!"가 출력됩니다. 왜 이런 일이 일어나는 걸까요?

     

    이 문제는 Go 언어에서 nil 포인터를 인터페이스에 넣을 때 발생하는 특성 때문입니다.

     

    Go에서는 명시적인 형 변환이 필요하지만, 인터페이스 타입으로 변환할 때는 암묵적으로 변환이 이루어집니다.

     

    위 코드에서 F1()의 반환값은 *MyErr 타입이지만, F2()에서 반환할 때는 error 타입(인터페이스)으로 암묵적으로 변환됩니다.

     

    Go의 nil 리터럴은 포인터로서의 nil인터페이스로서의 nil 두 가지 의미를 가집니다.

     

    인터페이스로서의 error 타입은 nil과 비교하여 에러 여부를 판단하는데, 포인터로서 nil을 넣으면 이것이 nil이 아님에도 불구하고 에러로 처리되는 경우가 발생할 수 있습니다.

     

    이러한 문제는 컴파일 단계에서는 걸러지지 않기 때문에, 코드 상에서 쉽게 눈에 띄지도 않습니다.

     

    그래서 정적 분석을 통해 이러한 오류를 미리 탐지하는 것이 중요합니다.


    정적 분석 도구 만들기

    이 문제를 해결하기 위해 직접 정적 분석 도구를 만들어 보았습니다.

     

    이 도구는 Go정적 분석 도구(skeleton)를 사용해 구현했는데요, 먼저 코드에서 포인터가 error 타입에 할당되는 위치를 찾아내는 것이 목표입니다.

    대상 코드

    코드에서 포인터가 error 타입에 할당되는 경우는 다음과 같은 경우입니다:

    1. 변수에 할당
    2. return
    3. 함수 인자

    이 중에서 이번에는 변수 할당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의 정적 분석 도구를 사용하면, 이러한 오류를 컴파일 전에 쉽게 찾아낼 수 있죠.

     

    처음 정적 분석을 시도해본 결과, 예상보다 간단하게 구현할 수 있었고, 여러분에게도 큰 도움이 되길 바랍니다!

     


     

Designed by Tistory.