这里我试图从包含字符串的切片为我的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链接。
获得结果的最佳方法是什么?
答案 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()
。造成这种情况的主要原因是它在引擎盖下使用(必须使用)反射。