如何枚举特定类型的常量

huangapple go评论80阅读模式
英文:

How to enumerate constants of a certain type

问题

我想通过一个测试来确保以下定义的每个APIErrorCode常量,在APIErrorCodeMessages映射中都包含一个条目。在Go语言中,如何枚举某种类型的所有常量?

// APIErrorCode表示API错误代码
type APIErrorCode int

const (
    // APIErrorCodeAuthentication表示身份验证错误,对应HTTP 401
    APIErrorCodeAuthentication APIErrorCode = 1000
    // APIErrorCodeInternalError表示未知的内部错误,对应HTTP 500
    APIErrorCodeInternalError APIErrorCode = 1001
)

// APIErrorCodeMessages保存APIErrorCodes的所有错误消息
var APIErrorCodeMessages = map[APIErrorCode]string{
    APIErrorCodeInternalError: "Internal Error",
}

我已经研究了reflectgo/importer,并尝试了tools/cmd/stringer,但没有成功。

英文:

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.

答案1

得分: 3

基本概念

reflect 包不提供对导出标识符的访问,因为不能保证它们将链接到可执行二进制文件(因此在运行时可用);更多信息请参考:https://stackoverflow.com/questions/38875016/splitting-client-server-code/38875901#38875901 和 https://stackoverflow.com/questions/42825926/how-to-remove-unused-code-at-compile-time/42827979#42827979

这是一个源代码级别的检查。我会编写一个测试来检查错误代码常量的数量是否与映射长度相匹配。下面的解决方案只会检查映射的长度。改进的版本(见下文)还可以检查映射中的键是否与常量声明的值匹配。

你可以使用 go/parser 解析包含错误代码常量的 Go 文件,它会给你一个描述文件的 ast.File,其中包含常量声明。你只需要遍历它,并计算错误代码常量的数量。

假设你的原始文件名为 "errcodes.go",编写一个名为 "errcodes_test.go" 的测试文件。

测试函数可以如下所示:

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
    // 遍历声明:
    for _, dd := range f.Decls {
        if gd, ok := dd.(*ast.GenDecl); ok {
            // 找到常量声明:
            if gd.Tok == token.CONST {
                for _, sp := range gd.Specs {
                    if valSp, ok := sp.(*ast.ValueSpec); ok {
                        for _, name := range valSp.Names {
                            // 计算以 "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)
    }
}

运行 go test 将得到以下结果:

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

该测试正确显示了有 2 个常量错误代码声明,但 APIErrorCodeMessages 映射只包含 1 个条目。

如果我们现在“补全”映射:

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

再次运行 go test

PASS

注意:这是一种风格问题,但是可以通过以下方式编写大循环来减少嵌套层级:

// 遍历声明:
for _, dd := range f.Decls {
    gd, ok := dd.(*ast.GenDecl)
    if !ok {
        continue
    }
    // 找到常量声明:
    if gd.Tok != token.CONST {
        continue
    }
    for _, sp := range gd.Specs {
        valSp, ok := sp.(*ast.ValueSpec)
        if !ok {
            continue
        }
        for _, name := range valSp.Names {
            // 计算以 "APIErrorCode" 开头的常量
            if strings.HasPrefix(name.Name, "APIErrorCode") {
                errCodeCount++
            }
        }
    }
}

完整、改进的检测

这次我们将检查常量的确切类型,而不是它们的名称。我们还将收集所有常量值,并在最后检查每个常量值是否在映射中。如果有遗漏,我们将打印遗漏代码的确切值。

下面是代码:

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
    // 遍历声明:
    for _, dd := range f.Decls {
        gd, ok := dd.(*ast.GenDecl)
        if !ok {
            continue
        }
        // 找到常量声明:
        if gd.Tok != token.CONST {
            continue
        }
        for _, sp := range gd.Specs {
            // 过滤 APIErrorCode 类型:
            valSp, ok := sp.(*ast.ValueSpec)
            if !ok {
                continue
            }
            if id, ok2 := valSp.Type.(*ast.Ident); !ok2 ||
                id.Name != "APIErrorCode" {
                continue
            }
            // 收集常量值到 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)
        }
    }
}

使用一个“不完整”的 APIErrorCodeMessages 映射运行 go test,我们会得到以下输出:

--- FAIL: TestMap (0.00s)
    errcodes_test.go:58: Could not found key in map: 1000
英文:

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: https://stackoverflow.com/questions/38875016/splitting-client-server-code/38875901#38875901; and https://stackoverflow.com/questions/42825926/how-to-remove-unused-code-at-compile-time/42827979#42827979

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

答案2

得分: 0

除了静态代码分析生成测试之外,你无法做到。

你只需要在某个地方维护一个已知类型的列表。最明显的地方可能是在你的测试中:

func TestAPICodes(t *testing.T) {
    for _, code := range []APIErrorCode{APIErrorCodeAuthentication, ...} {
        // 在这里进行测试
    }
}

如果你想将列表定义得更接近代码定义,你也可以将其放在主包中:

// APIErrorCode 表示 API 错误码
type APIErrorCode int

const (
    // APIErrorCodeAuthentication 表示身份验证错误,对应 HTTP 401
    APIErrorCodeAuthentication APIErrorCode = 1000
    // APIErrorCodeInternalError 表示未知的内部错误,对应 HTTP 500
    APIErrorCodeInternalError APIErrorCode = 1001
)

var allCodes = []APIErrorCode{APIErrorCodeAuthentication, ...}

或者,如果你确信你的 APIErrorCodeMessages 映射将保持更新,那么你已经有了解决方案。只需在测试中循环遍历该映射:

func TestAPICodes(t *testing.T) {
    for code := range APIErrorCodeMessages {
        // 进行测试...
    }
}
英文:

Short of static code analysis, which generates your tests, you can't.

You'll just need to maintain a list of known types somewhere. The most obvious place is probably in your test:

func TestAPICodes(t *testing.T) {
    for _, code := range []APIErrorCode{APIErrorCodeAuthentication, ...} {
        // Do your test here
    }
}

If you want the list defined closer to the code definitions, you could also put it in your main package:

// 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
)

var allCodes = []APIErrorCode{APIErrorCodeAuthentication, ...}

Or, if you're confident that your APIErrorCodeMessages map will be kept up-to-date, then you already have the solution. Just loop over that map in your test:

func TestAPICodes(t *testing.T) {
    for code := range APIErrorCodeMessages {
        // Do your tests...
    }
}

huangapple
  • 本文由 发表于 2017年4月24日 17:26:27
  • 转载请务必保留本文链接:https://go.coder-hub.com/43584377.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定