目录

Golang 如何操作 excel xlsx(包含创建/修改/读取/常见问题)

在日常的后端开发工作中,我们经常会遇到需要处理 Excel 文件的场景,比如生成数据报表、读取用户上传的 Excel 数据进行批量处理等。那么如何在 Go 项目中高效地操作 Excel XLSX 文件呢?。本文将记录分享最近使用的一个操作 xlsx 的库,希望能帮你少走一些弯路。

一、使用 excelize 库

Go 语言本身没有内置的 Excel 处理功能,我们需要依赖第三方库。目前,处理 XLSX (Excel 2007 及以后版本) 最主流和成熟的库是 github.com/xuri/excelize/v2

注意:该库的 github 会自动重定向为:https://github.com/qax-os/excelize

excelize 功能强大,支持创建新 Excel 文件、读取现有文件、修改单元格内容、设置单元格样式、合并单元格、添加图表等多种操作。

安装

go get github.com/xuri/excelize/v2

接下来,我们通过几个具体常用的例子来学习如何使用 excelize 进行常见的 Excel 操作。

二、创建并写入

这是最基本的操作。创建一个包含工作表、表头和一些数据的 Excel 文件

package main

import (
    "fmt"

    "github.com/xuri/excelize/v2"
)

func main() {
    // 创建一个新的 Excel 文件
    f := excelize.NewFile()
    defer func() {
        // 关闭文件
        if err := f.Close(); err != nil {
            fmt.Println(err)
        }
    }()

    // 获取默认的工作表,默认名为 "Sheet1"
    sheetName := "Sheet1"
    
    // 或者,你也可以创建一个新的工作表
    // newSheetName := "我的数据表"
    // index, err := f.NewSheet(newSheetName)
    // if err != nil {
    //     fmt.Println(err)
    //     return
    // }
    // f.SetActiveSheet(index) // 设置为活动工作表
    // sheetName = newSheetName

    // 设置表头
    headers := []string{"姓名", "年龄", "城市", "邮箱"}
    for col, header := range headers {
        // Excel 的列是从 A 开始的,行是从 1 开始的
        // 使用 excelize.CoordinatesToCellName 可以将行列号转换为单元格名称,如 (1,1) -> "A1"
        cell, err := excelize.CoordinatesToCellName(col+1, 1)
        if err != nil {
            fmt.Println(err)
            return
        }
        if err := f.SetCellValue(sheetName, cell, header); err != nil {
            fmt.Println(err)
            return
        }
    }

    // 写入数据
    data := [][]interface{}{
        {"张三", 25, "北京", "zhangsan@example.com"},
        {"李四", 30, "上海", "lisi@example.com"},
        {"王五", 22, "广州", "wangwu@example.com"},
    }

    for row, rowData := range data {
        for col, cellValue := range rowData {
            // 数据从第 2 行开始写入
            cell, err := excelize.CoordinatesToCellName(col+1, row+2)
            if err != nil {
                fmt.Println(err)
                return
            }
            if err := f.SetCellValue(sheetName, cell, cellValue); err != nil {
                fmt.Println(err)
                return
            }
        }
    }

    // 保存文件
    if err := f.SaveAs("example.xlsx"); err != nil {
        fmt.Println(err)
    } else {
        fmt.Println("Excel 文件创建成功!")
    }
}
  • excelize.NewFile(): 创建一个新的空白 Excel 文件对象。
  • f.NewSheet("表名"): 创建一个新的工作表。
  • f.SetCellValue(sheetName, cell, value): 向指定工作表的指定单元格写入值。cell 可以是 “A1” 这样的字符串,也可以通过 excelize.CoordinatesToCellName(row, col) 转换得到。
  • f.SaveAs("文件名.xlsx"): 将文件保存到指定路径。

三、读取 excel 文件

以下例子是读取上一步创建的 example.xlsx 文件中的数据:

package main

import (
    "fmt"

    "github.com/xuri/excelize/v2"
)

func main() {
    // 打开一个现有的 Excel 文件
    f, err := excelize.OpenFile("example.xlsx")
    if err != nil {
        fmt.Println(err)
        return
    }
    defer func() {
        // 关闭文件
        if err := f.Close(); err != nil {
            fmt.Println(err)
        }
    }()

    // 获取所有工作表名称
    sheetNames := f.GetSheetMap()
    fmt.Println("工作表名称列表:", sheetNames)

    // 选择要读取的工作表,这里我们读取第一个工作表
    sheetName := sheetNames[1] // sheetNames 是 map[int]string, key 是索引,从 1 开始
    fmt.Printf("正在读取工作表: %s\n", sheetName)

    // 获取工作表的最大行数和列数
    // 注意:GetRows 会自动忽略空行,但 GetSheetDimension 会返回包含空行的范围
    // rows, err := f.GetRows(sheetName)
    // if err != nil {
    //     fmt.Println(err)
    //     return
    // }
    // fmt.Printf("工作表 %s 共有 %d 行数据\n", sheetName, len(rows))

    // 使用 GetSheetDimension 获取更准确的范围
    dimension, err := f.GetSheetDimension(sheetName)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Printf("工作表维度: %s\n", dimension) // 例如:A1:D4

    // 按行读取数据
    rows, err := f.GetRows(sheetName, excelize.Options{RawCellValue: false})
    if err != nil {
        fmt.Println(err)
        return
    }

    for rowIndex, row := range rows {
        fmt.Printf("第 %d 行: ", rowIndex+1)
        for colIndex, cellValue := range row {
            // cellValue 是字符串类型
            fmt.Printf("列%d:%s ", colIndex+1, cellValue)
        }
        fmt.Println()
    }

    // 也可以直接读取指定单元格的值
    cellValue, err := f.GetCellValue(sheetName, "A2")
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Printf("单元格 A2 的值是: %s\n", cellValue) // 应该输出 "张三"
}
  • excelize.OpenFile("文件名.xlsx"): 打开一个已存在的 Excel 文件。
  • f.GetSheetMap(): 获取所有工作表的名称及其索引。
  • f.GetRows(sheetName): 获取指定工作表的所有行数据,返回一个二维字符串切片。excelize.Options{RawCellValue: false} 表示获取单元格格式化后的值。
  • f.GetCellValue(sheetName, cell): 获取指定单元格的值。

四、修改 excel 内容

在读取文件的基础上,修改其中的数据并保存:

package main

import (
    "fmt"

    "github.com/xuri/excelize/v2"
)

func main() {
    // 打开一个现有的 Excel 文件
    f, err := excelize.OpenFile("example.xlsx")
    if err != nil {
        fmt.Println(err)
        return
    }
    defer func() {
        if err := f.Close(); err != nil {
            fmt.Println(err)
        }
    }()

    sheetName := "Sheet1"

    // 修改一个单元格的值
    if err := f.SetCellValue(sheetName, "D3", "updated_lisi@example.com"); err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println("单元格 D3 (李四的邮箱) 已修改。")

    // 增加一行新数据
    newRow := []interface{}{"赵六", 28, "深圳", "zhaoliu@example.com"}
    // 获取当前数据的最后一行
    rows, err := f.GetRows(sheetName)
    if err != nil {
        fmt.Println(err)
        return
    }
    nextRow := len(rows) + 1

    for col, cellValue := range newRow {
        cell, err := excelize.CoordinatesToCellName(col+1, nextRow)
        if err != nil {
            fmt.Println(err)
            return
        }
        if err := f.SetCellValue(sheetName, cell, cellValue); err != nil {
            fmt.Println(err)
            return
        }
    }
    fmt.Println("已新增一行数据 (赵六)。")

    // 保存修改后的文件,可以覆盖原文件,也可以保存为新文件
    if err := f.SaveAs("example_modified.xlsx"); err != nil {
        fmt.Println(err)
    } else {
        fmt.Println("Excel 文件修改并保存成功!")
    }
}

这个例子是在 OpenFile 的基础上,使用 SetCellValue 修改了 D3 单元格的值,并在数据末尾添加了一行新数据,最后用 SaveAs 保存为新文件

常见问题

1. 中文乱码

问:写入 Excel 的中文字符显示为 ? 或乱码

解:在写入中文前,为单元格或样式设置支持中文的字体,如 “SimSun” (宋体), “Microsoft YaHei” (微软雅黑)

// 示例:创建一个样式并设置中文字体
style, err := f.NewStyle(`{"font":{"name":"SimSun","size":12}}`)
if err != nil {
    fmt.Println(err)
    return
}
// 将样式应用到单元格 A1
if err := f.SetCellStyle(sheetName, "A1", "A1", style); err != nil {
    fmt.Println(err)
    return
}

2. 数字格式问题(如小数、日期)

问:写入的数字(如 123.45)在 Excel 中显示为科学计数法或文本格式;写入的日期(如 time.Now())显示为数字

解:使用 SetCellValue 传入正确类型的值(如 float64(123.45) 而不是字符串 "123.45")。对于日期,可以先格式化,或者使用 SetCellFormula 设置公式,或者通过样式设置数字格式

// 示例:设置日期格式
dateStyle, err := f.NewStyle(`{"number_format": 22}`) // 22 对应 'm/d/yyyy' 格式,也可以用自定义格式如 `"yyyy-mm-dd"`
if err != nil {
    fmt.Println(err)
    return
}
f.SetCellValue(sheetName, "E2", time.Now())
f.SetCellStyle(sheetName, "E2", "E2", dateStyle)

3. 大文件处理时内存占用过高

:处理包含大量行和列的 Excel 文件时,程序内存占用急剧上升,甚至 OOM

:考虑使用流式读写。excelize 从 v2.4.0 版本开始提供了 OpenReaderStreamingReader 支持流式读取,如果只是读取数据,可以考虑使用更底层的库或者分块处理,

对于写入大文件,excelize 的性能通常不错,但也要注意数据构造的方式,避免在内存中创建过大的切片。

4. 合并单元格的读取问题

问:读取合并单元格区域时,只有左上角的单元格有值,其他合并的单元格为空

解:这是正常的,需要开发者在读取时,先检测单元格是否被合并 (f.GetMergeCells(sheetName)),然后根据合并区域来获取值

5. 文件保存后无法打开或提示损坏

问:程序运行没有报错,但生成的 Excel 文件无法用 Excel 或 WPS 打开,或打开时提示文件已损坏

解:检查 SaveAs 的返回错误,确保文件被正确保存和关闭。如果怀疑是库的问题,可以尝试更新 excelize 到最新版本

一般都是程序代码写入存在问题

总结

excelize 的功能远不止于此,它还支持设置单元格样式、边框、填充色、添加图片、创建图表等高级功能。你可以查阅其官方文档来获取更详细的信息,希望本文对你的开发有所帮助,欢迎大家在评论区留言交流!

版权声明

未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!

本文原文链接: https://fiveyoboy.com/articles/go-xlsx-excelize/

备用原文链接: https://blog.fiveyoboy.com/articles/go-xlsx-excelize/