使用切片值的 Golang 字符串格式
Golang string format using slice values
我在这里尝试从包含字符串的切片中为我的 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)
}
这里是goplaygroundlink.
获得结果的最佳方法是什么?
由于 string
个串联,您的解决方案使用了太多分配。
我们将创建一些替代方案,更快 and/or 更优雅的解决方案。请注意,以下解决方案不会检查节点值是否包含引号 "
字符。如果他们愿意,则必须以某种方式转义那些(否则结果将是无效的查询字符串)。
完整的 运行nable 代码可以在 Go Playground. The complete testing / benchmarking code can also be found on the Go Playground 上找到,但它不是 运行nable,将两者都保存到您的 Go 工作区(例如 $GOPATH/src/query/query.go
和 $GOPATH/src/query/query_test.go
) 和 运行 它与 go test -bench .
.
另外一定要查看这个相关问题:How to efficiently concatenate strings in 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)
}
使用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.Buffer
仍会进行一些重新分配,尽管比您原来的解决方案要少得多。
但是,如果我们在使用 bytes.NewBuffer()
创建 bytes.Buffer
时传递足够大的字节片,我们仍然可以将分配减少到 1。我们可以先计算出需要的大小:
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":"
连接起来,并应用适当的前缀和后缀。
需要注意的重要事项:strings.Join()
使用带有单个预分配 []byte
缓冲区的内置 copy()
函数,因此速度非常快! "As a special case, it (the copy()
function) also will copy bytes from a string to a slice of bytes."
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
一些不足为奇的事实:buildBuffer()
比 buildOriginal()
快 3.6 倍,并且 buildBuffer2()
(具有预先计算的大小)约为30% 比 buildBuffer()
快,因为它不需要重新分配(和复制)内部缓冲区。
一些令人惊讶的事实:buildJoin()
非常快,甚至比 buildBuffer2()
快 2.4 倍(因为只使用 []byte
和copy()
)。另一方面,buildTemplate()
证明相当慢:7 倍 比 buildOriginal()
慢。这样做的主要原因是因为它在引擎盖下使用(必须使用)反射。
我在这里尝试从包含字符串的切片中为我的 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)
}
这里是goplaygroundlink.
获得结果的最佳方法是什么?
由于 string
个串联,您的解决方案使用了太多分配。
我们将创建一些替代方案,更快 and/or 更优雅的解决方案。请注意,以下解决方案不会检查节点值是否包含引号 "
字符。如果他们愿意,则必须以某种方式转义那些(否则结果将是无效的查询字符串)。
完整的 运行nable 代码可以在 Go Playground. The complete testing / benchmarking code can also be found on the Go Playground 上找到,但它不是 运行nable,将两者都保存到您的 Go 工作区(例如 $GOPATH/src/query/query.go
和 $GOPATH/src/query/query_test.go
) 和 运行 它与 go test -bench .
.
另外一定要查看这个相关问题:How to efficiently concatenate strings in 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)
}
使用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.Buffer
仍会进行一些重新分配,尽管比您原来的解决方案要少得多。
但是,如果我们在使用 bytes.NewBuffer()
创建 bytes.Buffer
时传递足够大的字节片,我们仍然可以将分配减少到 1。我们可以先计算出需要的大小:
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":"
连接起来,并应用适当的前缀和后缀。
需要注意的重要事项:strings.Join()
使用带有单个预分配 []byte
缓冲区的内置 copy()
函数,因此速度非常快! "As a special case, it (the copy()
function) also will copy bytes from a string to a slice of bytes."
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
一些不足为奇的事实:buildBuffer()
比 buildOriginal()
快 3.6 倍,并且 buildBuffer2()
(具有预先计算的大小)约为30% 比 buildBuffer()
快,因为它不需要重新分配(和复制)内部缓冲区。
一些令人惊讶的事实:buildJoin()
非常快,甚至比 buildBuffer2()
快 2.4 倍(因为只使用 []byte
和copy()
)。另一方面,buildTemplate()
证明相当慢:7 倍 比 buildOriginal()
慢。这样做的主要原因是因为它在引擎盖下使用(必须使用)反射。