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

Jam*_*pam 5 string format go 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)
}
Run Code Online (Sandbox Code Playgroud)

这是goplayground链接.

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

icz*_*cza 12

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

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

完整的可运行代码可以在Go Playground上找到.完整的测试/基准测试代码也可以在Go Playground上找到,但它不可运行,只保存到Go工作区(例如$GOPATH/src/query/query.go$GOPATH/src/query/query_test.go)并运行它go test -bench ..

另外一定要看看这个相关的问题:如何在Go中有效地连接字符串?

备择方案

创世纪

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

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)
}
Run Code Online (Sandbox Code Playgroud)

运用 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()
}
Run Code Online (Sandbox Code Playgroud)

使用它:

nodes := []string{"node1", "node2"}
fmt.Println(buildBuffer(nodes))
Run Code Online (Sandbox Code Playgroud)

输出:

where={"node_name":"node1","node_name":"node2"}
Run Code Online (Sandbox Code Playgroud)

bytes.Buffer 改善

bytes.Buffer 仍然会进行一些重新分配,尽管比原来的解决方案要少得多.

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

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()
}
Run Code Online (Sandbox Code Playgroud)

请注意,在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 -}}
}`
Run Code Online (Sandbox Code Playgroud)

运用 strings.Join()

由于其简单性,该解决方案很有趣.我们可以使用strings.Join()中间的静态文本连接节点,","node_name":"应用正确的前缀和后缀.

需要注意的重要事项是:strings.Join()将内置copy()函数与单个预分配[]byte缓冲区一起使用,因此速度非常快!"作为一种特殊情况,它(copy()函数)也会将字符串中的字节复制到一个字节片段."

func buildJoin(nodes []string) string {
    if len(nodes) == 0 {
        return "where={}"
    }
    return `where={"node_name":"` + strings.Join(nodes, `","node_name":"`) + `"}`
}
Run Code Online (Sandbox Code Playgroud)

基准测试结果

我们将使用以下nodes值进行基准测试:

var nodes = []string{"n1", "node2", "nodethree", "fourthNode",
    "n1", "node2", "nodethree", "fourthNode",
    "n1", "node2", "nodethree", "fourthNode",
    "n1", "node2", "nodethree", "fourthNode",
    "n1", "node2", "nodethree", "fourthNode",
}
Run Code Online (Sandbox Code Playgroud)

基准测试代码如下所示:

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
Run Code Online (Sandbox Code Playgroud)

现在的结果是:

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
Run Code Online (Sandbox Code Playgroud)

有些令人吃惊的事实:buildBuffer()3.6倍的速度比buildOriginal(),和buildBuffer2()(同预先计算的大小)约30%的速度比buildBuffer(),因为它并不需要重新分配(并复制)的内部缓冲区.

一些令人惊讶的事实:buildJoin()非常快,甚至击败buildBuffer2()2.4倍(因为只使用到[]bytecopy()).buildTemplate()从另一方面证明相当缓慢:7次buildOriginal().造成这种情况的主要原因是它在引擎盖下使用(必须使用)反射.

  • 从你的回答中学到了很多东西,非常感谢。希望其他很多 golang 人也能从这个答案中学习。 (2认同)