Skip to content

Commit e0f0d41

Browse files
authored
Fix data race and optimize performance (#183)
1 parent 38f9eac commit e0f0d41

File tree

5 files changed

+147
-77
lines changed

5 files changed

+147
-77
lines changed

.travis.yml

+4-4
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@ language: go
33
matrix:
44
include:
55
- go: 1.9.x
6-
env: PKG='./...'
6+
env: GO111MODULE=on
77
- go: 1.12.x
8-
env: PKG='./...' COVER='-coverprofile=coverage.txt -covermode=count'
8+
env: GO111MODULE=on COVER='-coverprofile=coverage.txt -covermode=atomic'
99

1010
install:
11-
- go get -t -v $PKG
11+
- go get -t -v ./...
1212

1313
script:
14-
- go test $COVER $PKG
14+
- go test -race $COVER ./...
1515

1616
after_success:
1717
- bash <(curl -s https://codecov.io/bash)

i18n/bundle_test.go

+29
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package i18n
22

33
import (
4+
"fmt"
45
"reflect"
56
"testing"
67

@@ -31,6 +32,34 @@ var everythingMessage = MustNewMessage(map[string]string{
3132
"other": "other translation",
3233
})
3334

35+
func TestConcurrentAccess(t *testing.T) {
36+
bundle := NewBundle(language.English)
37+
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
38+
bundle.MustParseMessageFileBytes([]byte(`
39+
# Comment
40+
hello = "world"
41+
`), "en.toml")
42+
43+
count := 10
44+
errch := make(chan error, count)
45+
for i := 0; i < count; i++ {
46+
go func() {
47+
localized := NewLocalizer(bundle, "en").MustLocalize(&LocalizeConfig{MessageID: "hello"})
48+
if localized != "world" {
49+
errch <- fmt.Errorf(`expected "world"; got %q`, localized)
50+
} else {
51+
errch <- nil
52+
}
53+
}()
54+
}
55+
56+
for i := 0; i < count; i++ {
57+
if err := <-errch; err != nil {
58+
t.Fatal(err)
59+
}
60+
}
61+
}
62+
3463
func TestPseudoLanguage(t *testing.T) {
3564
bundle := NewBundle(language.English)
3665
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)

i18n/message_template.go

+13-20
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package i18n
22

33
import (
4-
"bytes"
54
"fmt"
65

76
"text/template"
@@ -19,12 +18,12 @@ type MessageTemplate struct {
1918
// NewMessageTemplate returns a new message template.
2019
func NewMessageTemplate(m *Message) *MessageTemplate {
2120
pluralTemplates := map[plural.Form]*internal.Template{}
22-
setPluralTemplate(pluralTemplates, plural.Zero, m.Zero)
23-
setPluralTemplate(pluralTemplates, plural.One, m.One)
24-
setPluralTemplate(pluralTemplates, plural.Two, m.Two)
25-
setPluralTemplate(pluralTemplates, plural.Few, m.Few)
26-
setPluralTemplate(pluralTemplates, plural.Many, m.Many)
27-
setPluralTemplate(pluralTemplates, plural.Other, m.Other)
21+
setPluralTemplate(pluralTemplates, plural.Zero, m.Zero, m.LeftDelim, m.RightDelim)
22+
setPluralTemplate(pluralTemplates, plural.One, m.One, m.LeftDelim, m.RightDelim)
23+
setPluralTemplate(pluralTemplates, plural.Two, m.Two, m.LeftDelim, m.RightDelim)
24+
setPluralTemplate(pluralTemplates, plural.Few, m.Few, m.LeftDelim, m.RightDelim)
25+
setPluralTemplate(pluralTemplates, plural.Many, m.Many, m.LeftDelim, m.RightDelim)
26+
setPluralTemplate(pluralTemplates, plural.Other, m.Other, m.LeftDelim, m.RightDelim)
2827
if len(pluralTemplates) == 0 {
2928
return nil
3029
}
@@ -34,9 +33,13 @@ func NewMessageTemplate(m *Message) *MessageTemplate {
3433
}
3534
}
3635

37-
func setPluralTemplate(pluralTemplates map[plural.Form]*internal.Template, pluralForm plural.Form, src string) {
36+
func setPluralTemplate(pluralTemplates map[plural.Form]*internal.Template, pluralForm plural.Form, src, leftDelim, rightDelim string) {
3837
if src != "" {
39-
pluralTemplates[pluralForm] = &internal.Template{Src: src}
38+
pluralTemplates[pluralForm] = &internal.Template{
39+
Src: src,
40+
LeftDelim: leftDelim,
41+
RightDelim: rightDelim,
42+
}
4043
}
4144
}
4245

@@ -58,15 +61,5 @@ func (mt *MessageTemplate) Execute(pluralForm plural.Form, data interface{}, fun
5861
messageID: mt.Message.ID,
5962
}
6063
}
61-
if err := t.Parse(mt.LeftDelim, mt.RightDelim, funcs); err != nil {
62-
return "", err
63-
}
64-
if t.Template == nil {
65-
return t.Src, nil
66-
}
67-
var buf bytes.Buffer
68-
if err := t.Template.Execute(&buf, data); err != nil {
69-
return "", err
70-
}
71-
return buf.String(), nil
64+
return t.Execute(funcs, data)
7265
}

internal/template.go

+38-13
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,51 @@
11
package internal
22

33
import (
4+
"bytes"
45
"strings"
6+
"sync"
57
gotemplate "text/template"
68
)
79

810
// Template stores the template for a string.
911
type Template struct {
10-
Src string
11-
Template *gotemplate.Template
12-
ParseErr *error
12+
Src string
13+
LeftDelim string
14+
RightDelim string
15+
16+
parseOnce sync.Once
17+
parsedTemplate *gotemplate.Template
18+
parseError error
1319
}
1420

15-
func (t *Template) Parse(leftDelim, rightDelim string, funcs gotemplate.FuncMap) error {
16-
if t.ParseErr == nil {
17-
if strings.Contains(t.Src, leftDelim) {
18-
gt, err := gotemplate.New("").Funcs(funcs).Delims(leftDelim, rightDelim).Parse(t.Src)
19-
t.Template = gt
20-
t.ParseErr = &err
21-
} else {
22-
t.ParseErr = new(error)
23-
}
21+
func (t *Template) Execute(funcs gotemplate.FuncMap, data interface{}) (string, error) {
22+
leftDelim := t.LeftDelim
23+
if leftDelim == "" {
24+
leftDelim = "{{"
25+
}
26+
if !strings.Contains(t.Src, leftDelim) {
27+
// Fast path to avoid parsing a template that has no actions.
28+
return t.Src, nil
29+
}
30+
31+
var gt *gotemplate.Template
32+
var err error
33+
if funcs == nil {
34+
t.parseOnce.Do(func() {
35+
// If funcs is nil, then we only need to parse this template once.
36+
t.parsedTemplate, t.parseError = gotemplate.New("").Delims(t.LeftDelim, t.RightDelim).Parse(t.Src)
37+
})
38+
gt, err = t.parsedTemplate, t.parseError
39+
} else {
40+
gt, err = gotemplate.New("").Delims(t.LeftDelim, t.RightDelim).Funcs(funcs).Parse(t.Src)
41+
}
42+
43+
if err != nil {
44+
return "", err
45+
}
46+
var buf bytes.Buffer
47+
if err := gt.Execute(&buf, data); err != nil {
48+
return "", err
2449
}
25-
return *t.ParseErr
50+
return buf.String(), nil
2651
}

internal/template_test.go

+63-40
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,77 @@
11
package internal
22

33
import (
4-
"bytes"
5-
"fmt"
64
"testing"
75
"text/template"
86
)
97

10-
func TestParse(t *testing.T) {
11-
tmpl := &Template{Src: "hello"}
12-
if err := tmpl.Parse("", "", nil); err != nil {
13-
t.Fatal(err)
14-
}
15-
if tmpl.ParseErr == nil {
16-
t.Fatal("expected non-nil parse error")
17-
}
18-
if tmpl.Template == nil {
19-
t.Fatal("expected non-nil template")
8+
func TestExecute(t *testing.T) {
9+
tests := []struct {
10+
template *Template
11+
funcs template.FuncMap
12+
data interface{}
13+
result string
14+
err string
15+
noallocs bool
16+
}{
17+
{
18+
template: &Template{
19+
Src: "hello",
20+
},
21+
result: "hello",
22+
noallocs: true,
23+
},
24+
{
25+
template: &Template{
26+
Src: "hello {{.Noun}}",
27+
},
28+
data: map[string]string{
29+
"Noun": "world",
30+
},
31+
result: "hello world",
32+
},
33+
{
34+
template: &Template{
35+
Src: "hello {{world}}",
36+
},
37+
funcs: template.FuncMap{
38+
"world": func() string {
39+
return "world"
40+
},
41+
},
42+
result: "hello world",
43+
},
44+
{
45+
template: &Template{
46+
Src: "hello {{",
47+
},
48+
err: "template: :1: unexpected unclosed action in command",
49+
noallocs: true,
50+
},
2051
}
21-
}
2252

23-
func TestParseError(t *testing.T) {
24-
expectedErr := fmt.Errorf("expected error")
25-
tmpl := &Template{ParseErr: &expectedErr}
26-
if err := tmpl.Parse("", "", nil); err != expectedErr {
27-
t.Fatalf("expected %#v; got %#v", expectedErr, err)
53+
for _, test := range tests {
54+
t.Run(test.template.Src, func(t *testing.T) {
55+
result, err := test.template.Execute(test.funcs, test.data)
56+
if actual := str(err); actual != test.err {
57+
t.Errorf("expected err %q; got %q", test.err, actual)
58+
}
59+
if result != test.result {
60+
t.Errorf("expected result %q; got %q", test.result, result)
61+
}
62+
allocs := testing.AllocsPerRun(10, func() {
63+
_, _ = test.template.Execute(test.funcs, test.data)
64+
})
65+
if test.noallocs && allocs > 0 {
66+
t.Errorf("expected no allocations; got %f", allocs)
67+
}
68+
})
2869
}
2970
}
3071

31-
func TestParseWithFunc(t *testing.T) {
32-
tmpl := &Template{Src: "{{foo}}"}
33-
funcs := template.FuncMap{
34-
"foo": func() string {
35-
return "bar"
36-
},
37-
}
38-
if err := tmpl.Parse("", "", funcs); err != nil {
39-
t.Fatal(err)
40-
}
41-
if tmpl.ParseErr == nil {
42-
t.Fatal("expected non-nil parse error")
43-
}
44-
if tmpl.Template == nil {
45-
t.Fatal("expected non-nil template")
46-
}
47-
var buf bytes.Buffer
48-
if tmpl.Template.Execute(&buf, nil) != nil {
49-
t.Fatal("expected nil template execute error")
50-
}
51-
if buf.String() != "bar" {
52-
t.Fatalf("expected bar; got %s", buf.String())
72+
func str(err error) string {
73+
if err == nil {
74+
return ""
5375
}
76+
return err.Error()
5477
}

0 commit comments

Comments
 (0)