Skip to content

Commit 8de777e

Browse files
committed
Write directly into the underlying writer without temporary buffer when generating json ouptut
1 parent 2ea0b4f commit 8de777e

File tree

3 files changed

+66
-46
lines changed

3 files changed

+66
-46
lines changed

jsonstring.go

Lines changed: 41 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,69 @@
11
package quicktemplate
22

3-
// appendJSONString is a synonym to strconv.AppendQuote, but works 3x faster.
4-
func appendJSONString(dst []byte, s string) []byte {
5-
j := 0
3+
import (
4+
"io"
5+
)
6+
7+
func writeJSONString(w io.Writer, s string) {
8+
write := w.Write
69
b := unsafeStrToBytes(s)
10+
j := 0
711
for i, n := 0, len(b); i < n; i++ {
812
switch b[i] {
913
case '"':
10-
dst = append(dst, b[j:i]...)
11-
dst = append(dst, `\"`...)
14+
write(b[j:i])
15+
write(strBackslashQuote)
1216
j = i + 1
1317
case '\\':
14-
dst = append(dst, b[j:i]...)
15-
dst = append(dst, `\\`...)
18+
write(b[j:i])
19+
write(strBackslashBackslash)
1620
j = i + 1
1721
case '\n':
18-
dst = append(dst, b[j:i]...)
19-
dst = append(dst, `\n`...)
22+
write(b[j:i])
23+
write(strBackslashN)
2024
j = i + 1
2125
case '\r':
22-
dst = append(dst, b[j:i]...)
23-
dst = append(dst, `\r`...)
26+
write(b[j:i])
27+
write(strBackslashR)
2428
j = i + 1
2529
case '\t':
26-
dst = append(dst, b[j:i]...)
27-
dst = append(dst, `\t`...)
30+
write(b[j:i])
31+
write(strBackslashT)
2832
j = i + 1
2933
case '\f':
30-
dst = append(dst, b[j:i]...)
31-
dst = append(dst, `\u000c`...)
34+
write(b[j:i])
35+
write(strBackslashF)
3236
j = i + 1
3337
case '\b':
34-
dst = append(dst, b[j:i]...)
35-
dst = append(dst, `\u0008`...)
38+
write(b[j:i])
39+
write(strBackslashB)
3640
j = i + 1
3741
case '<':
38-
dst = append(dst, b[j:i]...)
39-
dst = append(dst, `\u003c`...)
42+
write(b[j:i])
43+
write(strBackslashLT)
4044
j = i + 1
4145
case '\'':
42-
dst = append(dst, b[j:i]...)
43-
dst = append(dst, `\u0027`...)
46+
write(b[j:i])
47+
write(strBackslashQ)
4448
j = i + 1
4549
case 0:
46-
dst = append(dst, b[j:i]...)
47-
dst = append(dst, `\u0000`...)
50+
write(b[j:i])
51+
write(strBackslashZero)
4852
j = i + 1
4953
}
5054
}
51-
return append(dst, b[j:]...)
55+
write(b[j:])
5256
}
57+
58+
var (
59+
strBackslashQuote = []byte(`\"`)
60+
strBackslashBackslash = []byte(`\\`)
61+
strBackslashN = []byte(`\n`)
62+
strBackslashR = []byte(`\r`)
63+
strBackslashT = []byte(`\t`)
64+
strBackslashF = []byte(`\u000c`)
65+
strBackslashB = []byte(`\u0008`)
66+
strBackslashLT = []byte(`\u003c`)
67+
strBackslashQ = []byte(`\u0027`)
68+
strBackslashZero = []byte(`\u0000`)
69+
)

jsonstring_test.go

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,30 +6,34 @@ import (
66
"testing"
77
)
88

9-
func TestAppendJSONString(t *testing.T) {
10-
testAppendJSONString(t, ``)
11-
testAppendJSONString(t, `f`)
12-
testAppendJSONString(t, `"`)
13-
testAppendJSONString(t, `<`)
14-
testAppendJSONString(t, "\x00\n\r\t\b\f"+`"\`)
15-
testAppendJSONString(t, `"foobar`)
16-
testAppendJSONString(t, `foobar"`)
17-
testAppendJSONString(t, `foo "bar"
9+
func TestWriteJSONString(t *testing.T) {
10+
testWriteJSONString(t, ``)
11+
testWriteJSONString(t, `f`)
12+
testWriteJSONString(t, `"`)
13+
testWriteJSONString(t, `<`)
14+
testWriteJSONString(t, "\x00\n\r\t\b\f"+`"\`)
15+
testWriteJSONString(t, `"foobar`)
16+
testWriteJSONString(t, `foobar"`)
17+
testWriteJSONString(t, `foo "bar"
1818
baz`)
19-
testAppendJSONString(t, `this is a "тест"`)
20-
testAppendJSONString(t, `привет test`)
19+
testWriteJSONString(t, `this is a "тест"`)
20+
testWriteJSONString(t, `привет test`)
2121

22-
testAppendJSONString(t, `</script><script>alert('evil')</script>`)
22+
testWriteJSONString(t, `</script><script>alert('evil')</script>`)
2323
}
2424

25-
func testAppendJSONString(t *testing.T, s string) {
25+
func testWriteJSONString(t *testing.T, s string) {
2626
expectedResult, err := json.Marshal(s)
2727
if err != nil {
2828
t.Fatalf("unexpected error when encoding string %q: %s", s, err)
2929
}
3030
expectedResult = expectedResult[1 : len(expectedResult)-1]
3131

32-
result := string(appendJSONString(nil, s))
32+
bb := AcquireByteBuffer()
33+
writeJSONString(bb, s)
34+
result := string(bb.B)
35+
ReleaseByteBuffer(bb)
36+
3337
if strings.Contains(result, "'") {
3438
t.Fatalf("json string shouldn't contain single quote: %q, src %q", result, s)
3539
}

writer.go

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -98,13 +98,14 @@ func (w *QWriter) FPrec(f float64, prec int) {
9898

9999
// Q writes quoted json-safe s to w.
100100
func (w *QWriter) Q(s string) {
101-
writeQuick(w.w, func(dst []byte) []byte {
102-
dst = append(dst, '"')
103-
dst = appendJSONString(dst, s)
104-
return append(dst, '"')
105-
})
101+
ww := w.w
102+
ww.Write(strQuote)
103+
writeJSONString(w.w, s)
104+
ww.Write(strQuote)
106105
}
107106

107+
var strQuote = []byte(`"`)
108+
108109
// QZ writes quoted json-safe z to w.
109110
func (w *QWriter) QZ(z []byte) {
110111
w.Q(unsafeBytesToStr(z))
@@ -114,9 +115,7 @@ func (w *QWriter) QZ(z []byte) {
114115
//
115116
// Unlike Q it doesn't qoute resulting s.
116117
func (w *QWriter) J(s string) {
117-
writeQuick(w.w, func(dst []byte) []byte {
118-
return appendJSONString(dst, s)
119-
})
118+
writeJSONString(w.w, s)
120119
}
121120

122121
// JZ writes json-safe z to w.

0 commit comments

Comments
 (0)