diff --git a/.travis.yml b/.travis.yml index 03825a808979f7f11942e480bd9d19c8d4674d0c..a5c55f3ea57fbee3ad1c90ab6f8fab1c8336949c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,7 +21,7 @@ env: script: - env GO111MODULE=on go vet ./... - - env GO111MODULE=on go test ./... -v -coverprofile=coverage.txt -covermode=atomic + - env GO111MODULE=on go test -v -race ./... -coverprofile=coverage.txt -covermode=atomic after_success: - bash <(curl -s https://codecov.io/bash) diff --git a/cell.go b/cell.go index 3293d19fe4c02d67f10f4ce711e4a120eae541bf..383c02cf49386910bbf75d6eae9ab276617f1c75 100644 --- a/cell.go +++ b/cell.go @@ -18,7 +18,6 @@ import ( "reflect" "strconv" "strings" - "sync" "time" ) @@ -33,8 +32,6 @@ const ( STCellFormulaTypeShared = "shared" ) -var rwMutex sync.RWMutex - // GetCellValue provides a function to get formatted value from cell by given // worksheet name and axis in XLSX file. If it is possible to apply a format // to the cell value, it will do so, if not then an error will be returned, @@ -181,8 +178,6 @@ func setCellDuration(value time.Duration) (t string, v string) { // SetCellInt provides a function to set int type value of a cell by given // worksheet name, cell coordinates and cell value. func (f *File) SetCellInt(sheet, axis string, value int) error { - rwMutex.Lock() - defer rwMutex.Unlock() xlsx, err := f.workSheetReader(sheet) if err != nil { return err @@ -204,8 +199,6 @@ func setCellInt(value int) (t string, v string) { // SetCellBool provides a function to set bool type value of a cell by given // worksheet name, cell name and cell value. func (f *File) SetCellBool(sheet, axis string, value bool) error { - rwMutex.Lock() - defer rwMutex.Unlock() xlsx, err := f.workSheetReader(sheet) if err != nil { return err @@ -239,8 +232,6 @@ func setCellBool(value bool) (t string, v string) { // f.SetCellFloat("Sheet1", "A1", float64(x), 2, 32) // func (f *File) SetCellFloat(sheet, axis string, value float64, prec, bitSize int) error { - rwMutex.Lock() - defer rwMutex.Unlock() xlsx, err := f.workSheetReader(sheet) if err != nil { return err @@ -262,8 +253,6 @@ func setCellFloat(value float64, prec, bitSize int) (t string, v string) { // SetCellStr provides a function to set string type value of a cell. Total // number of characters that a cell can contain 32767 characters. func (f *File) SetCellStr(sheet, axis, value string) error { - rwMutex.Lock() - defer rwMutex.Unlock() xlsx, err := f.workSheetReader(sheet) if err != nil { return err @@ -291,6 +280,8 @@ func (f *File) setCellString(value string) (t string, v string) { // setSharedString provides a function to add string to the share string table. func (f *File) setSharedString(val string) int { sst := f.sharedStringsReader() + f.Lock() + defer f.Unlock() if i, ok := f.sharedStringsMap[val]; ok { return i } @@ -371,8 +362,6 @@ type FormulaOpts struct { // SetCellFormula provides a function to set cell formula by given string and // worksheet name. func (f *File) SetCellFormula(sheet, axis, formula string, opts ...FormulaOpts) error { - rwMutex.Lock() - defer rwMutex.Unlock() xlsx, err := f.workSheetReader(sheet) if err != nil { return err @@ -697,6 +686,8 @@ func (f *File) SetSheetRow(sheet, axis string, slice interface{}) error { // getCellInfo does common preparation for all SetCell* methods. func (f *File) prepareCell(xlsx *xlsxWorksheet, sheet, cell string) (*xlsxC, int, int, error) { + xlsx.Lock() + defer xlsx.Unlock() var err error cell, err = f.mergeCellsParser(xlsx, cell) if err != nil { @@ -728,6 +719,9 @@ func (f *File) getCellStringFunc(sheet, axis string, fn func(x *xlsxWorksheet, c return "", err } + xlsx.Lock() + defer xlsx.Unlock() + lastRowNum := 0 if l := len(xlsx.SheetData.Row); l > 0 { lastRowNum = xlsx.SheetData.Row[l-1].R diff --git a/cell_test.go b/cell_test.go index fb30596f1878dbc6cf7b3cb60f5702fafb9dfa7f..ba4cd83b0945273942cca117ec2e4c6bcde17dc9 100644 --- a/cell_test.go +++ b/cell_test.go @@ -4,12 +4,33 @@ import ( "fmt" "path/filepath" "strconv" + "sync" "testing" "time" "github.com/stretchr/testify/assert" ) +func TestConcurrency(t *testing.T) { + f := NewFile() + wg := new(sync.WaitGroup) + for i := 1; i <= 5; i++ { + wg.Add(1) + go func(val int) { + f.SetCellValue("Sheet1", fmt.Sprintf("A%d", val), val) + f.SetCellValue("Sheet1", fmt.Sprintf("B%d", val), strconv.Itoa(val)) + f.GetCellValue("Sheet1", fmt.Sprintf("A%d", val)) + wg.Done() + }(i) + } + wg.Wait() + val, err := f.GetCellValue("Sheet1", "A1") + if err != nil { + t.Error(err) + } + assert.Equal(t, "1", val) +} + func TestCheckCellInArea(t *testing.T) { f := NewFile() expectedTrueCellInAreaList := [][2]string{ diff --git a/excelize.go b/excelize.go index 5cc88e961f2e72ad6c5b217e6127cfe98dc1ebfe..bfb3abaad4ee26ad8a82aa73ab0a293d4e3e2e24 100644 --- a/excelize.go +++ b/excelize.go @@ -24,12 +24,14 @@ import ( "path" "strconv" "strings" + "sync" "golang.org/x/net/html/charset" ) // File define a populated spreadsheet file struct. type File struct { + sync.RWMutex xmlAttr map[string][]xml.Attr checked map[string]bool sheetMap map[string]string @@ -153,6 +155,8 @@ func (f *File) setDefaultTimeStyle(sheet, axis string, format int) error { // workSheetReader provides a function to get the pointer to the structure // after deserialization by given worksheet name. func (f *File) workSheetReader(sheet string) (xlsx *xlsxWorksheet, err error) { + f.Lock() + defer f.Unlock() var ( name string ok bool @@ -323,7 +327,7 @@ func (f *File) AddVBAProject(bin string) error { var err error // Check vbaProject.bin exists first. if _, err = os.Stat(bin); os.IsNotExist(err) { - return err + return fmt.Errorf("stat %s: no such file or directory", bin) } if path.Ext(bin) != ".bin" { return errors.New("unsupported VBA project extension") diff --git a/lib.go b/lib.go index acb4590142eecd700786f0fb17eabf1b592bc0c8..88aa3a117adcd11e76aaa6b28441da25c99a9106 100644 --- a/lib.go +++ b/lib.go @@ -167,7 +167,7 @@ func ColumnNumberToName(num int) (string, error) { } var col string for num > 0 { - col = string((num-1)%26+65) + col + col = string(rune((num-1)%26+65)) + col num = (num - 1) / 26 } return col, nil diff --git a/rows.go b/rows.go index 320ba2fdf26e66c29e6f2d7c3023cc895893bcb3..66dd16becf80041a59d5fd7b9bddcd5cc621d9e3 100644 --- a/rows.go +++ b/rows.go @@ -284,6 +284,8 @@ func (f *File) GetRowHeight(sheet string, row int) (float64, error) { func (f *File) sharedStringsReader() *xlsxSST { var err error + f.Lock() + defer f.Unlock() if f.SharedStrings == nil { var sharedStrings xlsxSST ss := f.readXML("xl/sharedStrings.xml") @@ -318,6 +320,8 @@ func (f *File) sharedStringsReader() *xlsxSST { // inteded to be used with for range on rows an argument with the xlsx opened // file. func (xlsx *xlsxC) getValueFrom(f *File, d *xlsxSST) (string, error) { + f.Lock() + defer f.Unlock() switch xlsx.T { case "s": if xlsx.V != "" { diff --git a/stream_test.go b/stream_test.go index d89dad845a1f1aa492d6c51b26583ee6239b2bb8..d81b1d4b137777eb145b885cfef6a285d2c6c3ce 100644 --- a/stream_test.go +++ b/stream_test.go @@ -91,6 +91,7 @@ func TestStreamWriter(t *testing.T) { assert.NoError(t, err) _, err = streamWriter.rawData.Reader() assert.NoError(t, err) + assert.NoError(t, streamWriter.rawData.tmp.Close()) assert.NoError(t, os.Remove(streamWriter.rawData.tmp.Name())) // Test unsupport charset diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 7cd73c4185aca59c21a46631c7d4f1fc8eb276f1..2b39e64dfc1bdc516cf513a4648e3244a0b8426a 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -11,11 +11,15 @@ package excelize -import "encoding/xml" +import ( + "encoding/xml" + "sync" +) // xlsxWorksheet directly maps the worksheet element in the namespace // http://schemas.openxmlformats.org/spreadsheetml/2006/main. type xlsxWorksheet struct { + sync.RWMutex XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main worksheet"` SheetPr *xlsxSheetPr `xml:"sheetPr"` Dimension *xlsxDimension `xml:"dimension"`