Golang string format using slice values

Here I am trying to create a query string for my API from a slice containing strings.

i.e. 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) } 

Here is the goplayground link.

What is the best way to get the result?

+5
source share
1 answer

Your solution uses too many allocations due to string concatenations.

We will create alternative, faster and / or more elegant solutions. Note that the solutions below do not check if the node values ​​contain the quotation mark. " If that were the case, they would have to be somehow avoided (otherwise the result would be an invalid query string).

Full, executable code can be found on the Go Playground . The full test / benchmarking code can also be found on the Go Playground , but it will not run, save both in the Go workspace (for example, $GOPATH/src/query/query.go and $GOPATH/src/query/query_test.go ) and run it with go test -bench . .

Also be sure to check out this related question: How to efficiently concatenate strings in Go?

Alternatives

Genesis

Your logic can be written using the following function:

 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) } 

Using bytes.Buffer

It would be much better to use one buffer, for example. bytes.Buffer , build a query in this and convert it to string at the end:

 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() } 

Using it:

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

Output:

 where={"node_name":"node1","node_name":"node2"} 

bytes.Buffer improved

bytes.Buffer will still perform some redistributions, although much less than your original solution.

However, we can still reduce the selection to 1 if, when creating bytes.Buffer we pass a sufficiently large byte fragment using bytes.NewBuffer() . We can calculate the required size up to:

 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() } 

Notice that in size calculating 8 is the size of the where={} , and 15 is the size of the string "node_name":"", .

Using text/template

We can also create a text template and use text/template to execute it, effectively generating the result:

 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 -}} }` 

Using strings.Join()

This solution is interesting because of its simplicity. We can use strings.Join() to join the nodes with the static text ","node_name":" between them, the corresponding prefix and postfix.

It is important to note: strings.Join() uses the built-in copy() function with one pre-allocated buffer []byte , so it is very fast! "As a special case, it (the copy() function) will also copy bytes from the string to a piece of byte."

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

Test results

We compare the following values ​​of nodes :

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

And the comparison code is as follows:

 func BenchmarkOriginal(b *testing.B) { for i := 0; i < bN; i++ { buildOriginal(nodes) } } func BenchmarkBuffer(b *testing.B) { for i := 0; i < bN; i++ { buildBuffer(nodes) } } // ... All the other benchmarking functions look the same 

And now the results:

 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 

Some unsurprising facts: buildBuffer() 3.6 times faster than buildOriginal() , and buildBuffer2() (with a pre-calculated size) is about 30% faster than buildBuffer() because it does not need to redistribute (and copy) the internal buffer.

Some amazing facts: buildJoin() extremely fast, even surpasses buildBuffer2() by 2.4 times (due to using only []byte and copy() ). buildTemplate() , on the other hand, turned out to be rather slow: 7 times slower than buildOriginal() . The main reason for this is that it uses (should use) the reflection under the hood.

+11
source

Source: https://habr.com/ru/post/1262262/


All Articles