diff --git a/csv.go b/csv.go index aeb6f85..4b57286 100644 --- a/csv.go +++ b/csv.go @@ -16,6 +16,7 @@ import ( "reflect" "strings" "sync" + "sync/atomic" ) // FailIfUnmatchedStructTags indicates whether it is considered an error when there is an unmatched @@ -47,17 +48,36 @@ type Normalizer func(string) string type ErrorHandler func(*csv.ParseError) bool -// normalizeName function initially set to a nop Normalizer. -var normalizeName = DefaultNameNormalizer() +// headerNormalization is the Normalizer in use together with the struct infos +// built with it. SetHeaderNormalizer replaces both at once, so it is safe to +// call while other goroutines are reading or writing CSV. +type headerNormalization struct { + normalize Normalizer + structInfos sync.Map +} + +// currentHeaderNormalization holds a *headerNormalization, initially with a nop Normalizer. +var currentHeaderNormalization atomic.Value + +func init() { + currentHeaderNormalization.Store(&headerNormalization{normalize: DefaultNameNormalizer()}) +} + +func loadHeaderNormalization() *headerNormalization { + return currentHeaderNormalization.Load().(*headerNormalization) +} + +func normalizeName(name string) string { + return loadHeaderNormalization().normalize(name) +} // DefaultNameNormalizer is a nop Normalizer. func DefaultNameNormalizer() Normalizer { return func(s string) string { return s } } // SetHeaderNormalizer sets the normalizer used to normalize struct and header field names. func SetHeaderNormalizer(f Normalizer) { - normalizeName = f - // Need to clear the cache hen the header normalizer changes. - structInfoCache = sync.Map{} + // The struct infos are rebuilt from scratch, as they hold normalized names. + currentHeaderNormalization.Store(&headerNormalization{normalize: f}) } // -------------------------------------------------------------------------- diff --git a/csv_test.go b/csv_test.go index 85390a6..f725bac 100644 --- a/csv_test.go +++ b/csv_test.go @@ -2,6 +2,8 @@ package gocsv import ( "errors" + "strings" + "sync" "testing" ) @@ -25,6 +27,36 @@ func TestUnmarshalToCallback_ReaderError(t *testing.T) { } } +func TestSetHeaderNormalizerWhileUnmarshaling(t *testing.T) { + defer SetHeaderNormalizer(DefaultNameNormalizer()) + + type row struct { + ID string `csv:"id"` + Name string `csv:"name"` + } + + var wg sync.WaitGroup + for i := 0; i < 50; i++ { + wg.Add(2) + go func() { + defer wg.Done() + SetHeaderNormalizer(strings.ToLower) + }() + go func() { + defer wg.Done() + var rows []row + if err := UnmarshalString("id,name\n1,foo\n", &rows); err != nil { + t.Error(err) + return + } + if len(rows) != 1 || rows[0].ID != "1" || rows[0].Name != "foo" { + t.Errorf("unexpected rows: %+v", rows) + } + }() + } + wg.Wait() +} + type errorReader struct{} func (e *errorReader) Read([]byte) (n int, err error) { diff --git a/reflect.go b/reflect.go index 5b72873..943dc59 100644 --- a/reflect.go +++ b/reflect.go @@ -58,19 +58,19 @@ func removeZeroWidthChars(s string) string { }, s) } -var structInfoCache sync.Map var structMap = make(map[reflect.Type]*structInfo) var structMapMutex sync.RWMutex func getStructInfo(rType reflect.Type) *structInfo { - stInfo, ok := structInfoCache.Load(rType) + structInfos := &loadHeaderNormalization().structInfos + stInfo, ok := structInfos.Load(rType) if ok { return stInfo.(*structInfo) } fieldsList, err := getFieldInfos(rType, []int{}, []string{}, make(map[reflect.Type]bool)) stInfo = &structInfo{Fields: fieldsList, Err: err} - structInfoCache.Store(rType, stInfo) + structInfos.Store(rType, stInfo) return stInfo.(*structInfo) }