使用切片值的Golang字符串格式

时间:2017-01-04 06:12:04

标签: string go format slice

这里我试图从包含字符串的切片为我的API创建一个查询字符串。

即。 where={"node_name":"node1","node_name":"node_2"}

import (
   "fmt"
   "strings"
)

func main() {
    nodes := []string{"node1", "node2"}
    var query string
    for _, n := range nodes {
        query += fmt.Sprintf("\"node_name\":\"%s\",", n)
    }
    query = strings.TrimRight(query, ",")
    final := fmt.Sprintf("where={%s}", query)
    fmt.Println(final)
}

以下是goplayground链接。

获得结果的最佳方法是什么?

1 个答案:

答案 0 :(得分:12)

由于string连接,您的解决方案使用了太多的分配。

我们将创建一些替代的,更快的和/或更优雅的解决方案。请注意,以下解决方案不会检查节点值是否包含引号"字符。如果他们愿意,那些必须以某种方式进行转义(否则结果将是无效的查询字符串)。

可以在Go Playground上找到完整的可运行代码。完整的测试/基准测试代码也可以在Go Playground上找到,但它不可运行,同时保存到Go工作区(例如$GOPATH/src/query/query.go$GOPATH/src/query/query_test.go)并使用{{运行它1}}。

另请务必查看以下相关问题:How to efficiently concatenate strings in Go?

替代

您的逻辑可以通过以下函数捕获:

go test -bench .

使用func buildOriginal(nodes []string) string { var query string for _, n := range nodes { query += fmt.Sprintf("\"node_name\":\"%s\",", n) } query = strings.TrimRight(query, ",") return fmt.Sprintf("where={%s}", query) }

更好的是使用单个缓冲区,例如bytes.Buffer,在其中构建查询,并在结尾处将其转换为bytes.Buffer

string

使用它:

func buildBuffer(nodes []string) string {
    buf := &bytes.Buffer{}
    buf.WriteString("where={")
    for i, v := range nodes {
        if i > 0 {
            buf.WriteByte(',')
        }
        buf.WriteString(`"node_name":"`)
        buf.WriteString(v)
        buf.WriteByte('"')
    }
    buf.WriteByte('}')
    return buf.String()
}

输出:

nodes := []string{"node1", "node2"}
fmt.Println(buildBuffer(nodes))

where={"node_name":"node1","node_name":"node2"} 已改进

bytes.Buffer仍会进行一些重新分配,但比原始解决方案要少得多。

但是,如果我们在使用bytes.NewBuffer()创建bytes.Buffer时传递足够大的字节切片,我们仍然可以将分配减少到1。我们可以先计算所需的尺寸:

bytes.Buffer

请注意,在func buildBuffer2(nodes []string) string { size := 8 + len(nodes)*15 for _, v := range nodes { size += len(v) } buf := bytes.NewBuffer(make([]byte, 0, size)) buf.WriteString("where={") for i, v := range nodes { if i > 0 { buf.WriteByte(',') } buf.WriteString(`"node_name":"`) buf.WriteString(v) buf.WriteByte('"') } buf.WriteByte('}') return buf.String() } 计算中,size是字符串8的大小,而where={}是字符串15的大小。

使用"node_name":"",

我们还可以创建一个文本模板,并使用text/template包来执行它,有效地生成结果:

text/template

使用var t = template.Must(template.New("").Parse(templ)) func buildTemplate(nodes []string) string { size := 8 + len(nodes)*15 for _, v := range nodes { size += len(v) } buf := bytes.NewBuffer(make([]byte, 0, size)) if err := t.Execute(buf, nodes); err != nil { log.Fatal(err) // Handle error } return buf.String() } const templ = `where={ {{- range $idx, $n := . -}} {{if ne $idx 0}},{{end}}"node_name":"{{$n}}" {{- end -}} }`

由于其简单性,此解决方案很有趣。我们可以使用strings.Join()加入节点,其间包含静态文本strings.Join(),正确的前缀和后缀应用。

需要注意的一点是:","node_name":"使用内置copy()函数和一个预先分配的strings.Join()缓冲区,所以速度非常快! "作为一种特殊情况,它([]byte函数)也会将字符串中的字节复制到一个字节片段。"

copy()

基准测试结果

我们将使用以下func buildJoin(nodes []string) string { if len(nodes) == 0 { return "where={}" } return `where={"node_name":"` + strings.Join(nodes, `","node_name":"`) + `"}` } 值进行基准测试:

nodes

基准测试代码如下所示:

var nodes = []string{"n1", "node2", "nodethree", "fourthNode",
    "n1", "node2", "nodethree", "fourthNode",
    "n1", "node2", "nodethree", "fourthNode",
    "n1", "node2", "nodethree", "fourthNode",
    "n1", "node2", "nodethree", "fourthNode",
}

现在的结果是:

func BenchmarkOriginal(b *testing.B) {
    for i := 0; i < b.N; i++ {
        buildOriginal(nodes)
    }
}

func BenchmarkBuffer(b *testing.B) {
    for i := 0; i < b.N; i++ {
        buildBuffer(nodes)
    }
}

// ... All the other benchmarking functions look the same

一些不足为奇的事实:BenchmarkOriginal-4 200000 10572 ns/op BenchmarkBuffer-4 500000 2914 ns/op BenchmarkBuffer2-4 1000000 2024 ns/op BenchmarkBufferTemplate-4 30000 77634 ns/op BenchmarkJoin-4 2000000 830 ns/op 3.6倍buildBuffer()快,buildOriginal()(预先计算的大小)约 30% buildBuffer2()快,因为它不需要重新分配(并复制)内部缓冲区。

一些令人惊讶的事实:buildBuffer()非常快,甚至超过{{1>} 2.4次(由于仅使用了buildJoin()buildBuffer2() )。另一方面,[]byte证明非常缓慢: 7次慢于copy()。造成这种情况的主要原因是它在引擎盖下使用(必须使用)反射。