Golang中如何定义列表类型?

Golang中如何定义列表类型? 我想定义一个预定义列表 ["one", "two", "three"] 的类型:

type T ????
func MyFunc(t T) {}

现在,当我调用 MyFunc 时,它应该允许使用 ["one"]["one", "two"]["three"] 等,但不允许使用 ["other value"]

MyFunc(["one"]) // 正确
MyFunc(["one","two","three"]) // 正确
MyFunc(["one", "other"]) // 不正确
MyFunc([""]) // 不正确

此外,不允许重复:

MyFunc(["one", "two", "one"]) // 不正确:"one" 重复了两次

顺序无关紧要:

MyFunc(["one", "two"]) // 正确
MyFunc(["two", "one"]) // 正确

那么,我们如何定义这种特定列表的类型呢?我想我需要使用接口来实现?不确定具体如何实现。


更多关于Golang中如何定义列表类型?的实战教程也可以访问 https://www.itying.com/category-94-b0.html

8 回复
// Compiles just fine
MyFunc("Bad value literal")

这正是我不希望出现的情况。这应该由编译器来处理。因此,如果传入了错误的值,它应该抛出一个错误。

更多关于Golang中如何定义列表类型?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


好的,那么在您的 TypeScript 示例中,您将如何防范这种情况?您如何防范我的 copyOpts 示例?无论如何,您都必须:

  1. 进行某种运行时检查。
  2. 当某些东西在运行时改变了这个值时,可能会产生一些不理想的结果。

如果您在 TypeScript 中会采用后一种做法,那就直接使用 type MyString string 这个选项。如果是前者,就进行某种运行时检查。您也可以使用上面 StackOverflow 答案中链接的未导出结构体方法,如果您愿意的话。

感谢您的回答。有没有类似类型或接口的东西?而不是手动抛出错误?

我正在寻找类似于TypeScript中的行为:

// TypeScript example
interface AnimationOptions {
  easing: "ease-in" | "ease-out" | "ease-in-out";
}

啊,抱歉。这个例子也没有完全满足我的需求,因为我想实现的是“其中之一”或“多个之一”的效果。但我正在寻找类似这样的东西。

skillian:

我不熟悉linter,但我怀疑可以定义一个能捕获这种情况并给出警告的linter。

我也有同样的想法,但同样对linter不够熟悉,不知道是否能为这种情况编写一个linting规则。TSC所做的或多或少相当于一个linting规则,这也是为什么它很容易被绕过(就像我上面提到的那样)。我在想是否可以使用类似这样的工具:

GitHub GitHub - quasilyte/go-ruleguard: 定义并运行基于模式的自定义linting规则

仓库图片

定义并运行基于模式的自定义linting规则。

并定义一个规则,禁止将字符串字面量传递给 MyFunc,这样就要求必须传入一个变量。在这种情况下,编译器会报错,提示你不能使用 string 类型作为 MyString 类型。

我的理解是:您希望将字符串限制为特定的值。也许可以按照以下思路实现?

// 用于存储有效值的全局变量
var acceptableStrings = []string{"one", "two", "three"}

func isValid(s string) bool {
	for _, v := range acceptableStrings {
		if v == s {
			return true
		}
	}
	return false
}

func MyFunc(items []string) error {
	for _, v := range items {
		if !isValid(v) {
			return fmt.Errorf("Invalid value: %v", v)
		}
	}
	return nil
}

然后进行测试:

func main() {
	testCases := [][]string{
		{"1", "2"},
		{"First", "Second", "Third"},
		{"one", "Second", "three"},
		{"one"},
		{"one", "two", "three"},
	}
	for _, v := range testCases {
		if err := MyFunc(v); err != nil {
			fmt.Println(err)
		} else {
			fmt.Println("Acceptable values:", v)
		}
	}
}

… 输出结果如下:

Invalid value: 1
Invalid value: First
Invalid value: Second
Acceptable values: [one]
Acceptable values: [one two three]

Program exited.

您可以在此处自行运行:

Go Playground - The Go Programming Language

啊——一个明确表达你意图的简单方法是创建一个类型化的字符串,如下所示:

type MyString string

const (
	MyStringOne   MyString = "one"
	MyStringTwo   MyString = "two"
	MyStringThree MyString = "three"
)

func MyFunc(v MyString) {
	fmt.Println(v)
}

好处:这清晰地表明了意图。坏处:你可以传入一个无效的字面量值,而编译器不会在意:

// 导致以下错误:
// cannot use s (variable of type string) as MyString value in argument to MyFunc
s := "bad value"
MyFunc(s)
// 编译完全没问题
MyFunc("Bad value literal")

查看这个答案以了解不同的方法:

创建常量类型并限制类型的值

另外——TypeScript 提供的安全性是一种编译时检查。但你可以很容易地覆盖它,所以我认为你不能真正指望那些值永远只是你接口中定义的值。考虑以下情况:

interface AnimationOptions {
  easing: "ease-in" | "ease-out" | "ease-in-out";
}

// 工作正常
let opts: AnimationOptions = {
    //@ts-ignore
    easing: "bad value"
};

// 工作正常
let second = copyOpts({
    easing : 'uh oh'
});

function copyOpts(opts: any) : AnimationOptions {
    return {...opts}
}

我的观点是:如果你想在你的 TypeScript 示例中获得真正的安全性,你仍然需要进行运行时检查。因为(至少根据我的经验)通常数据来自异步调用,而接口在那里提供了安全性的假象,但实际上几乎没有安全性。

另外,不允许重复:

MyFunc(["one", "two", "one"]) // 错误:"one" 重复了两次

我想不出在 Go 中处理这个问题的方法,但有一些变通方案可以部分解决,比如 Dean 建议的使用 const MyStringOne MyString,或者,如果你想确保无法使用其他字符串值,可以这样做:

type Something interface {
    // 任何以小写字母开头的方法,这样该接口就无法被此包之外的任何东西实现。
    something()
}

type one struct{}
func (one) something() { }
type two struct{}
func (two) something() { }
type three struct{}
func (three) something() { }

var (
    One Something = one{}
    Two Something = two{}
    Three Something = three{}
)

func MyFunc(somethings ...Something) {
    for _, s := range somethings {
        switch s {
        case One:
            // 执行某些操作
        case Two:
            // 执行某些操作
        case Three:
            // 执行某些操作
        default:
            // 无法确保匹配到完整集合。
            panic(fmt.Errorf("未知的 Something: %T", s))
        }
    }
}

func main() {
    MyFunc(One, Two)
}

在编译时,没有什么能阻止你写:

MyFunc(One, One)

我不熟悉 linter,但我怀疑可以定义一个 linter 来捕获这种情况并给出警告。

在 TypeScript 中有办法捕获这个吗?我想知道如果你有下面的代码会发生什么:

type Something = "one" | "two" | "three"

function MyFunc(somethings: Something[]) {
    somethings.push("one");
    return myFuncInternal(somethings)
}

function myFuncInternal(somethings: Something[]) {
}

在Go中实现这种约束需要使用类型系统和运行时验证的组合。以下是实现方案:

package main

import (
	"fmt"
	"strings"
)

// 定义允许的值集合
var allowedValues = map[string]bool{
	"one":   true,
	"two":   true,
	"three": true,
}

// 定义自定义类型
type T []string

// 验证函数
func validateT(t T) error {
	if len(t) == 0 {
		return fmt.Errorf("列表不能为空")
	}

	seen := make(map[string]bool)
	
	for _, value := range t {
		// 检查是否允许的值
		if !allowedValues[value] {
			return fmt.Errorf("值 '%s' 不在允许的列表中", value)
		}
		
		// 检查重复
		if seen[value] {
			return fmt.Errorf("值 '%s' 重复出现", value)
		}
		seen[value] = true
	}
	
	return nil
}

// 工厂函数创建验证后的T
func NewT(values ...string) (T, error) {
	t := T(values)
	if err := validateT(t); err != nil {
		return nil, err
	}
	return t, nil
}

// 必须使用的方法
func MyFunc(t T) {
	// 在函数入口处验证
	if err := validateT(t); err != nil {
		panic(err)
	}
	
	fmt.Printf("调用MyFunc: %v\n", t)
}

// 可选:为T类型添加方法
func (t T) String() string {
	return fmt.Sprintf("[%s]", strings.Join(t, ", "))
}

func main() {
	// 正确用例
	t1, _ := NewT("one")
	MyFunc(t1)
	
	t2, _ := NewT("one", "two")
	MyFunc(t2)
	
	t3, _ := NewT("two", "one")
	MyFunc(t3)
	
	t4, _ := NewT("one", "two", "three")
	MyFunc(t4)
	
	// 错误用例
	_, err1 := NewT("other")
	fmt.Println("错误1:", err1)
	
	_, err2 := NewT("one", "one")
	fmt.Println("错误2:", err2)
	
	_, err3 := NewT("")
	fmt.Println("错误3:", err3)
	
	// 直接使用字面量(不推荐)
	// MyFunc(T{"one", "two"}) // 这会在运行时panic
}

更严格的实现使用私有类型和构造函数:

package main

import (
	"fmt"
	"strings"
)

type t struct {
	values []string
}

func (t t) Values() []string {
	return t.values
}

func (t t) String() string {
	return fmt.Sprintf("[%s]", strings.Join(t.values, ", "))
}

func NewT(values ...string) (t, error) {
	allowed := map[string]bool{"one": true, "two": true, "three": true}
	seen := make(map[string]bool)
	
	if len(values) == 0 {
		return t{}, fmt.Errorf("列表不能为空")
	}
	
	for _, v := range values {
		if !allowed[v] {
			return t{}, fmt.Errorf("值 '%s' 不在允许的列表中", v)
		}
		if seen[v] {
			return t{}, fmt.Errorf("值 '%s' 重复出现", v)
		}
		seen[v] = true
	}
	
	return t{values: values}, nil
}

func MyFunc(t t) {
	fmt.Printf("调用MyFunc: %v\n", t.Values())
}

func main() {
	t1, _ := NewT("one")
	MyFunc(t1)
	
	t2, _ := NewT("one", "two")
	MyFunc(t2)
	
	// 编译时无法阻止的错误
	_, err := NewT("other")
	if err != nil {
		fmt.Println("错误:", err)
	}
}

如果需要编译时检查,可以使用代码生成工具,但会增加复杂性。上述方案在运行时提供验证,这是Go中处理此类约束的典型方式。

回到顶部