dongshi9719 2017-04-24 09:26
浏览 98
已采纳

如何枚举某种类型的常量

I'd like to ensure with a test, that for each APIErrorCode constant defined as below, the map APIErrorCodeMessages contains an entry. How can I enumerate all constants of a certain type in Go?

// APIErrorCode represents the API error code
type APIErrorCode int

const (
    // APIErrorCodeAuthentication represents an authentication error and corresponds with HTTP 401
    APIErrorCodeAuthentication APIErrorCode = 1000
    // APIErrorCodeInternalError represents an unknown internal error and corresponds with HTTP 500
    APIErrorCodeInternalError APIErrorCode = 1001
)

// APIErrorCodeMessages holds all error messages for APIErrorCodes
var APIErrorCodeMessages = map[APIErrorCode]string{
    APIErrorCodeInternalError: "Internal Error",
}

I've looked into reflect and go/importer and tried tools/cmd/stringer without success.

  • 写回答

2条回答 默认 最新

  • dongmeiyi2266 2017-04-24 11:22
    关注

    Basic concept

    The reflect package does not provide access to exported identifiers, as there is no guarantee they will be linked to the executable binary (and thus available at runtime); more on this: Splitting client/server code; and How to remove unused code at compile time?

    This is a source-code level checking. What I would do is write a test that checks if the number of error code constants matches the map length. The solution below will only check the map length. An improved version (see below) may also check if the keys in the map match the values of the constant declarations too.

    You may use the go/parser to parse the Go file containing the error code constants, which gives you an ast.File describing the file, containing the constant declarations. You just need to walk through it, and count the error code constant declarations.

    Let's say your original file is named "errcodes.go", write a test file named "errcodes_test.go".

    This is how the test function could look like:

    func TestMap(t *testing.T) {
        fset := token.NewFileSet()
        f, err := parser.ParseFile(fset, "errcodes.go", nil, 0)
        if err != nil {
            t.Errorf("Failed to parse file: %v", err)
            return
        }
    
        errCodeCount := 0
        // Range through declarations:
        for _, dd := range f.Decls {
            if gd, ok := dd.(*ast.GenDecl); ok {
                // Find constant declrations:
                if gd.Tok == token.CONST {
                    for _, sp := range gd.Specs {
                        if valSp, ok := sp.(*ast.ValueSpec); ok {
                            for _, name := range valSp.Names {
                                // Count those that start with "APIErrorCode"
                                if strings.HasPrefix(name.Name, "APIErrorCode") {
                                    errCodeCount++
                                }
                            }
                        }
                    }
                }
            }
        }
        if exp, got := errCodeCount, len(APIErrorCodeMessages); exp != got {
            t.Errorf("Expected %d err codes, got: %d", exp, got)
        }
    }
    

    Running go test will result in:

    --- FAIL: TestMap (0.00s)
        errcodes_test.go:39: Expected 2 err codes, got: 1
    

    The test properly reveals that there are 2 constant error code declarations, but the APIErrorCodeMessages map contains only 1 entry.

    If we now "complete" the map:

    var APIErrorCodeMessages = map[APIErrorCode]string{
        APIErrorCodeInternalError:  "Internal Error",
        APIErrorCodeAuthentication: "asdf",
    }
    

    And run go test again:

    PASS
    

    Note: it's a matter of style, but the big loop may be written this way to decrease nesting level:

    // Range through declarations:
    for _, dd := range f.Decls {
        gd, ok := dd.(*ast.GenDecl)
        if !ok {
            continue
        }
        // Find constant declrations:
        if gd.Tok != token.CONST {
            continue
        }
        for _, sp := range gd.Specs {
            valSp, ok := sp.(*ast.ValueSpec)
            if !ok {
                continue
            }
            for _, name := range valSp.Names {
                // Count those that start with "APIErrorCode"
                if strings.HasPrefix(name.Name, "APIErrorCode") {
                    errCodeCount++
                }
            }
        }
    }
    

    Full, improved detection

    This time we will check the exact type of the constants, not their names. We will also gather all the constant values, and in the end we will check each if that exact constant value is in the map. If something is missing, we will print the exact values of the missing codes.

    So here it is:

    func TestMap(t *testing.T) {
        fset := token.NewFileSet()
        f, err := parser.ParseFile(fset, "errcodes.go", nil, 0)
        if err != nil {
            t.Errorf("Failed to parse file: %v", err)
            return
        }
    
        var keys []APIErrorCode
        // Range through declarations:
        for _, dd := range f.Decls {
            gd, ok := dd.(*ast.GenDecl)
            if !ok {
                continue
            }
            // Find constant declrations:
            if gd.Tok != token.CONST {
                continue
            }
            for _, sp := range gd.Specs {
                // Filter by APIErrorCode type:
                valSp, ok := sp.(*ast.ValueSpec)
                if !ok {
                    continue
                }
                if id, ok2 := valSp.Type.(*ast.Ident); !ok2 ||
                    id.Name != "APIErrorCode" {
                    continue
                }
                // And gather the constant values in keys:
                for _, value := range valSp.Values {
                    bslit, ok := value.(*ast.BasicLit)
                    if !ok {
                        continue
                    }
                    keyValue, err := strconv.Atoi(bslit.Value)
                    if err != nil {
                        t.Errorf("Could not parse value from %v: %v",
                            bslit.Value, err)
                    }
                    keys = append(keys, APIErrorCode(keyValue))
                }
            }
        }
    
        for _, key := range keys {
            if _, found := APIErrorCodeMessages[key]; !found {
                t.Errorf("Could not found key in map: %v", key)
            }
        }
    }
    

    Running go test with an "incomplete" APIErrorCodeMessages map, we get the following output:

    --- FAIL: TestMap (0.00s)
        errcodes_test.go:58: Could not found key in map: 1000
    
    本回答被题主选为最佳回答 , 对您是否有帮助呢?
    评论
查看更多回答(1条)

报告相同问题?

悬赏问题

  • ¥15 基于卷积神经网络的声纹识别
  • ¥15 Python中的request,如何使用ssr节点,通过代理requests网页。本人在泰国,需要用大陆ip才能玩网页游戏,合法合规。
  • ¥100 为什么这个恒流源电路不能恒流?
  • ¥15 有偿求跨组件数据流路径图
  • ¥15 写一个方法checkPerson,入参实体类Person,出参布尔值
  • ¥15 我想咨询一下路面纹理三维点云数据处理的一些问题,上传的坐标文件里是怎么对无序点进行编号的,以及xy坐标在处理的时候是进行整体模型分片处理的吗
  • ¥15 CSAPPattacklab
  • ¥15 一直显示正在等待HID—ISP
  • ¥15 Python turtle 画图
  • ¥15 stm32开发clion时遇到的编译问题