无法将 []string 转换为 []interface{}。

118

我正在编写一些代码,需要捕获参数并将它们通过fmt.Println传递(我想要它的默认行为,即用空格分隔参数并在后面加上换行符)。但是flag.Args()返回[]string而不是[]interface{}。以下是示例代码:

package main

import (
    "fmt"
    "flag"
)

func main() {
    flag.Parse()
    fmt.Println(flag.Args()...)
}

这会返回以下错误:

./example.go:10: cannot use args (type []string) as type []interface {} in function argument

这是一个bug吗?fmt.Println不应该接受任何数组吗?顺便说一下,我也尝试过这样做:

var args = []interface{}(flag.Args())

但我得到了以下错误:

cannot convert flag.Args() (type []string) to type []interface {}

有没有一种“Go”方式可以解决这个问题?


1
我在尝试一个简单的例子(go run test.go some test flags)时,当将flags.Args()...更改为flag.Args()时,它似乎可以工作(输出是[some test flags],后跟换行符;也似乎可以使用实际标志进行注册)。不会假装理解为什么,而且Stephen的答案更加详细 :) - RocketDonkey
7个回答

139

这不是一个bug。fmt.Println()需要一个[]interface{}类型。也就是说,它必须是一个interface{}值的切片,而不是"任何切片"。为了转换这个切片,你需要循环并复制每个元素。

old := flag.Args()
new := make([]interface{}, len(old))
for i, v := range old {
    new[i] = v
}
fmt.Println(new...)
你不能使用任何切片的原因是将 []string[]interface{} 之间进行转换需要改变内存布局,并且会在 O(n) 的时间内完成。将类型转换为 interface{} 只需要 O(1) 的时间。如果他们使这个循环多余了,编译器仍然需要插入它。

2
顺便说一下,我在golang-nuts中找到了这个链接:https://groups.google.com/d/topic/golang-nuts/Il-tO1xtAyE/discussion - cruizh
2
是的,每次迭代需要O(1)时间,循环需要O(n)时间。这就是我说的。至于接收[]string的函数,它期望一个interface{}interface{}string具有不同的内存布局,因此需要转换每个元素是问题所在。 - Stephen Weinberg
2
@karlrh:不,假设Println函数修改了切片并设置了一些元素(实际上它没有这样做,但是假设它这样做)。然后它可以将任何interface{}放入切片中,而该切片应该只有string。你真正想要的是类似于Java泛型通配符Slice<? extends []interface{}>,但在Go中不存在。 - newacct
4
Append是一种神奇的机制,它是内置的并被语言特别对待。其他例子包括new()len()copy()。详见http://golang.org/ref/spec#Appending_and_copying_slices。 - Stephen Weinberg
1
今天我学到了'new'不是保留关键字!'make'也不是! - Sridhar
显示剩余3条评论

14

在这种情况下,类型转换是不必要的。只需将flag.Args()的值传递给fmt.Println即可。


问题:

无法将 []string 转换为 []interface {}

我正在编写一些代码,并且需要捕获参数并将它们传递给 fmt.Println (我想要其默认行为,即用空格分隔参数,后跟换行符)。

以下是代码示例:

package main

import (
    "fmt"
    "flag"
)

func main() {
    flag.Parse()
    fmt.Println(flag.Args()...)
}

flag 包

import "flag"

func Args

func Args() []string

Args 函数返回非标志命令行参数。


fmt 包

import "fmt"

func Println

func Println(a ...interface{}) (n int, err error)

Println 函数使用其操作数的默认格式进行格式化并写入标准输出。操作数之间始终添加空格,并追加一个换行符。它返回写入的字节数以及遇到的任何写入错误。


在这种情况下,不需要进行类型转换。只需将 flag.Args() 值传递给 fmt.Println 即可,后者使用反射将该值解释为类型 []string。包reflect 实现了运行时反射,允许程序操作具有任意类型的对象。例如,

args.go:

package main

import (
    "flag"
    "fmt"
)

func main() {
    flag.Parse()
    fmt.Println(flag.Args())
}

输出:

$ go build args.go
$ ./args arg0 arg1
[arg0 arg1]
$ 

如果我想省略括号,转换是否仍然是不必要的? - cruizh

11

如果你只想打印字符串的一部分,你可以通过连接字符串来避免转换并获得完全相同的输出:

package main

import (
    "fmt"
    "flag"
    "strings"
)

func main() {
    flag.Parse()
    s := strings.Join(flag.Args(), " ")
    fmt.Println(s)
}

0
在Go中,函数只能接受在函数定义的参数列表中指定的类型的参数。可变参数语言特性会使这有些复杂,但它遵循明确定义的规则。 fmt.Println 的函数签名为:
func Println(a ...interface{}) (n int, err error)

根据语言规范,

函数签名中的最后一个传入参数可以带有类型前缀.... 带有这种参数的函数称为变参函数,可以使用零个或多个参数为该参数调用。

这意味着你可以传递一个interface{}类型的参数列表给Println。由于所有类型都实现了空接口,因此您可以传递任何类型的参数列表,这就是您能够无误地调用例如Println(1, "one", true) 的原因。请参阅语言规范中的"将参数传递给...参数"部分

传递的值是具有新底层数组的类型[]T的新切片,其连续元素为实际参数,其中所有实参都必须可分配给T。

让您困扰的部分就在那之后的语言规范中的描述中:

如果最后一个参数可以分配给切片类型[]T,则如果该参数后跟...,则它可以作为... T参数的值传递而不更改。在这种情况下,不会创建新的切片。
flag.Args()的类型是[]string。由于Println中的T是interface{},所以[]T是[]interface{}。因此,问题归结为字符串切片值是否可分配给接口切片类型的变量。您可以通过尝试赋值来轻松测试go代码,例如:
s := []string{}
var i []interface{}
i = s

如果您尝试这样的任务,编译器将输出以下错误消息:
cannot use s (type []string) as type []interface {} in assignment

这就是为什么你不能在字符串切片后使用省略号作为fmt.Println的参数。这不是一个错误,它是按照预期工作的。

仍然有很多方法可以使用Println打印flag.Args(),例如:

fmt.Println(flag.Args())

(这将作为[elem0 elem1 ...]输出,参见fmt包文档

或者

fmt.Println(strings.Join(flag.Args(), ` `)

使用字符串包中的Join函数,并指定一个字符串分隔符,例如空格,来输出字符串切片元素。

0

另一个选项是仅迭代切片:

package main
import "flag"

func main() {
   flag.Parse()
   for _, each := range flag.Args() {
      println(each)
   }
}

0

我认为可以使用反射,但我不确定它是否是一个好的解决方案。

package main

import (
    "fmt"
    "reflect"
    "strings"
)

type User struct {
    Name string
    Age  byte
}

func main() {
    flag.Parse()
    fmt.Println(String(flag.Args()))
    fmt.Println(String([]string{"hello", "world"}))
    fmt.Println(String([]int{1, 2, 3, 4, 5, 6}))
    u1, u2 := User{Name: "John", Age: 30},
        User{Name: "Not John", Age: 20}
    fmt.Println(String([]User{u1, u2}))
}

func String(v interface{}) string {
    val := reflect.ValueOf(v)
    if val.Kind() == reflect.Array || val.Kind() == reflect.Slice {
        l := val.Len()
        if l == 0 {
            return ""
        }
        if l == 1 {
            return fmt.Sprint(val.Index(0))
        }
        sb := strings.Builder{}
        sb.Grow(l * 4)
        sb.WriteString(fmt.Sprint(val.Index(0)))
        for i := 1; i < l; i++ {
            sb.WriteString(",")
            sb.WriteString(fmt.Sprint(val.Index(i)))
        }
        return sb.String()
    }

    return fmt.Sprintln(v)
}

输出:

$ go run .\main.go arg1 arg2
arg1,arg2
hello,world
1,2,3,4,5,6
{John 30},{Not John 20}

-2

fmt.Println 接受 可变参数

func Println(a ...interface{}) (n int, err error)

可以直接打印 flag.Args() 而不需要转换为 []interface{}

func main() {
    flag.Parse()
    fmt.Println(flag.Args())
}

2
fmt.Println的签名已经超过6年没有改变了(那只是一个error包的更改)。即使它改变了,规范清楚地说明:如果带有类型为...T的最终参数p的可变参数,则在f内部,p的类型等效于类型[]T。所以这并不重要。fmt.Println(flags.Args()...)仍然无法工作(您错过了切片扩展),fmt.Println(flags.Args())始终可以工作。 - Marc
你是怎么得到这个信息的,说 fmt.Println 的签名在6年以上都没有变过?你有查看源代码吗? - Shahriar
1
Yup: https://github.com/golang/go/blame/release-branch.go1.10/src/fmt/print.go#L260(在1.10分支上) - Marc
op 上的评论表明,仅将 flag.Args() 作为 fmt.Println 的参数使用在那个时候是有效的。(如果当时没有这样做会很惊讶) - leaf bebop
那么,如果有评论,这意味着我的答案应该被踩吗? - Shahriar
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接