Description
Description
When using excelize/v2@v2.9.0 with Go 1.24.2, performing RemoveRow operations on a sheet containing formulas (e.g., =DATEDIF(A_cell,TODAY(),"d"),other untested) and then saving the file can lead to a corrupted XLSX file. Microsoft Excel reports issues with the file content, often related to /xl/calcChain.xml (Calculation Properties) and sometimes shared formulas.
Subsequently,if you open the file with Excel and repair&save it,then attempting to open this corrupted file with excelize and perform further RemoveRow operations on it causes a panic: runtime error: invalid memory address or nil pointer dereference. The panic occurs within the excelize.(*File).adjustCalcChain method, specifically at the loop header for i := 0; i < len(f.CalcChain.C); i++ { (which corresponds to line 850 in adjust.go for v2.9.0 in my pkg/mod directory), indicating f.CalcChain is likely nil at that point due to the prior file corruption.
This issue demonstrates a two-stage problem:
Initial file corruption caused by RemoveRow + SaveAs.
Subsequent panic when excelize attempts to process this self-corrupted file which recovered from excel.
当在 Go 1.24.2 环境下使用 excelize/v2@v2.9.0 时,对包含公式(例如 =DATEDIF(A_cell,TODAY(),"d"),其它未测试)的工作表执行 RemoveRow 操作并保存文件后,可能导致生成的 XLSX 文件损坏。Microsoft Excel 会报告文件内容存在问题,通常与 /xl/calcChain.xml(计算属性)相关,有时也与共享公式相关。
随后,如果此时使用excel打开该文件并进行修复保存,再尝试使用 excelize 打开这个已损坏的文件并对其执行进一步的 RemoveRow 操作时,会引发一个 panic: runtime error: invalid memory address or nil pointer dereference(运行时错误:无效的内存地址或空指针解引用)。此 panic发生在 excelize.(*File).adjustCalcChain 方法内部,具体是在循环头 for i := 0; i < len(f.CalcChain.C); i++ { 处(根据我 pkg/mod 目录中 v2.9.0 版本的 adjust.go 文件,这对应第 850 行),表明由于之前的文件损坏,f.CalcChain 在此时很可能为 nil。
此问题表现为一个两阶段过程:
首先,RemoveRow + SaveAs 操作导致了初始的文件损坏。
然后,当 excelize 尝试处理被excel修复的文件时,发生后续的 panic。
Steps to reproduce the issue
1. Environment / 环境:
Go version / Go 版本: 1.24.2
excelize version / excelize 版本: github.com/xuri/excelize/v2@v2.9.0
Operating System / 操作系统: Windows 11 24H2
2. Prepare Source Excel File / 准备源 Excel 文件:
Create an Excel file named test_source_AB.xlsx with the following structure on a sheet named Sheet1:
创建一个名为 test_source_AB.xlsx 的 Excel 文件,在名为 Sheet1 的工作表上包含以下结构:
Column A (Date) / A列 (日期) | Column B (Formula using DATEDIF & TODAY) / B列 (使用DATEDIF和TODAY的公式) |
---|---|
(Header) Date / (表头) 日期 | (Header) Days Diff / (表头) 天数差 |
2024-01-15 | =DATEDIF(A2,TODAY(),"d") |
2024-02-20 | =DATEDIF(A3,TODAY(),"d") |
2024-03-25 | =DATEDIF(A4,TODAY(),"d") |
2024-04-30 | =DATEDIF(A5,TODAY(),"d") |
Ensure cells in Column A are formatted as dates.
请确保 A 列的单元格在 Excel 中已格式化为日期。
3. Go Code to Reproduce / 用于复现的 Go 代码:
Save the following Go code (e.g., as excelize_bug_test.go) in the same directory as test_source_AB.xlsx:
将以下 Go 代码(例如,保存为 excelize_bug_test.go)与 test_source_AB.xlsx 文件放置在同一目录下:
package main
import (
"fmt"
"log"
"os"
"time"
"github.com/xuri/excelize/v2"//2.9.0
)
const (
manualSourceFile = "test_source_AB.xlsx" // Manually created file / 手动创建的文件
intermediateCorruptedFile = "intermediate_corrupted_AB.xlsx"
finalOutputFile = "final_output_AB.xlsx"
sheetName = "Sheet1"
)
func main() {
log.Println("--- Starting single-thread test for excelize bug ---")
log.Println("--- 开始 excelize bug 的单线程测试 ---")
log.Printf("Excelize target version: v2.9.0 (confirm via go.mod)")
log.Printf("Excelize 目标版本: v2.9.0 (请通过 go.mod 确认)")
log.Printf("Current date for TODAY(): %s", time.Now().Format(time.DateOnly))
log.Printf("TODAY() 使用的当前日期: %s", time.Now().Format(time.DateOnly))
log.Printf("IMPORTANT: Ensure '%s' is manually created as per instructions.", manualSourceFile)
log.Printf("重要提示: 请确保按照说明手动创建了 '%s' 文件。", manualSourceFile)
if _, err := os.Stat(manualSourceFile); os.IsNotExist(err) {
log.Fatalf("!!! Source file '%s' not found. Please create it and re-run.", manualSourceFile)
log.Fatalf("!!! 源文件 '%s' 未找到。请创建该文件后重新运行。", manualSourceFile)
}
// --- Phase 1: Generate the intermediate (likely corrupted) file ---
// --- 阶段 1: 生成中间(可能已损坏的)文件 ---
log.Println("\n--- Phase 1: Generating intermediate file (expect Excel to report corruption) ---")
log.Println("\n--- 阶段 1: 生成中间文件 (预期 Excel 会报告文件损坏) ---")
var successPhase1 bool
func() {
f1, err := excelize.OpenFile(manualSourceFile)
if err != nil {
log.Fatalf("Phase 1: Failed to open source file '%s': %v", manualSourceFile, err)
log.Fatalf("阶段 1: 打开源文件 '%s' 失败: %v", manualSourceFile, err)
return
}
defer f1.Close()
_, sheetCheckErr := f1.GetSheetIndex(sheetName)
if sheetCheckErr != nil {
log.Printf("!!! Phase 1: Sheet '%s' not found in '%s'.", sheetName, manualSourceFile)
log.Printf("!!! 阶段 1: 在 '%s' 文件中未找到工作表 '%s'。", sheetName, manualSourceFile)
return
}
rowToRemoveInPhase1 := 3 // Attempt to remove the 2nd data row (Excel row 3) / 尝试删除第2条数据行 (Excel 中的第3行)
log.Printf("Phase 1: Removing row %d from sheet '%s' in '%s'...", rowToRemoveInPhase1, sheetName, manualSourceFile)
log.Printf("阶段 1: 从 '%s' 的 '%s' 工作表中删除第 %d 行...", rowToRemoveInPhase1, sheetName, manualSourceFile)
if err := f1.RemoveRow(sheetName, rowToRemoveInPhase1); err != nil {
log.Printf("!!! Phase 1: f1.RemoveRow() returned an error: %v. This might indicate internal state issues.", err)
log.Printf("!!! 阶段 1: f1.RemoveRow() 调用返回错误: %v。这可能表明存在内部状态问题。", err)
} else {
log.Printf("Phase 1: f1.RemoveRow() call completed without direct error.")
log.Printf("阶段 1: f1.RemoveRow() 调用成功完成 (未直接返回错误)。")
}
log.Printf("Phase 1: Saving intermediate file as '%s'...", intermediateCorruptedFile)
log.Printf("阶段 1: 将中间文件保存为 '%s'...", intermediateCorruptedFile)
if err := f1.SaveAs(intermediateCorruptedFile); err != nil {
log.Fatalf("Phase 1: Failed to save intermediate file '%s': %v", intermediateCorruptedFile, err)
log.Fatalf("阶段 1: 保存中间文件 '%s' 失败: %v", intermediateCorruptedFile, err)
return
}
log.Printf("Phase 1: Intermediate file '%s' saved.", intermediateCorruptedFile)
log.Printf("阶段 1: 中间文件 '%s' 已保存。", intermediateCorruptedFile)
log.Println(">>> After Phase 1, try manually opening this intermediate file with Excel; expect a corruption warning.")
log.Println(">>> 阶段 1 完成后, 请尝试用 Excel 手动打开此中间文件;预期会看到损坏警告。")
successPhase1 = true
}()
if !successPhase1 {
log.Println("Phase 1 did not complete successfully. Test aborted.")
log.Println("阶段 1 未成功完成。测试中止。")
return
}
// --- Phase 2: Attempt to process the (likely corrupted) intermediate file ---
// --- 阶段 2: 尝试处理(可能已损坏的)中间文件 ---
log.Println("\n--- Phase 2: Processing intermediate file (expect potential panic) ---")
log.Println("\n--- 阶段 2: 处理中间文件 (预期可能发生 panic) ---")
func() {
**//Before executing this section, use Excel to open and repair the saved "test_datedif_intermediate_corrupted.xlsx" file.
//在此段执行前使用excel打开并修复保存“test_datedif_intermediate_corrupted.xlsx”文件。**
log.Printf("Phase 2: Opening intermediate file '%s'", intermediateCorruptedFile)
log.Printf("阶段 2: 打开中间文件 '%s'", intermediateCorruptedFile)
f2, err := excelize.OpenFile(intermediateCorruptedFile)
if err != nil {
log.Printf("!!! Phase 2: Failed to open intermediate file '%s': %v. This might be due to severe corruption.", intermediateCorruptedFile, err)
log.Printf("!!! 阶段 2: 打开中间文件 '%s' 失败: %v。这可能是由于文件严重损坏。", intermediateCorruptedFile, err)
return
}
defer f2.Close()
// After removing row 3 in Phase 1 (original Excel row 3, which was the 2nd data row),
// the sheet now has Header (row 1), original Data1 (now row 2), original Data3 (now row 3), etc.
// Let's try to remove the current row 2 (which was original Data1).
// 阶段 1 中删除了第 3 行(原始 Excel 的第3行,即第2条数据行)之后,
// 工作表现有表头(第1行)、原始数据1(现为第2行)、原始数据3(现为第3行)等。
// 我们尝试删除当前的第 2 行(即原始数据1所在行)。
rowToRemoveInPhase2 := 2
log.Printf("Phase 2: Attempting to remove row %d from sheet '%s' in '%s'...", rowToRemoveInPhase2, sheetName, intermediateCorruptedFile)
log.Printf("阶段 2: 尝试从 '%s' 的 '%s' 工作表中删除第 %d 行...", rowToRemoveInPhase2, sheetName, intermediateCorruptedFile)
// *****************************************************************************
// *** EXPECTED PANIC LOCATION: nil pointer dereference in adjustCalcChain ***
// *** 预期发生 PANIC 的位置: adjustCalcChain 中的空指针解引用 ***
// *****************************************************************************
if err := f2.RemoveRow(sheetName, rowToRemoveInPhase2); err != nil {
log.Printf("!!! Phase 2: f2.RemoveRow() returned an error: %v", err)
log.Printf("!!! 阶段 2: f2.RemoveRow() 调用返回错误: %v", err)
} else {
log.Printf("Phase 2: f2.RemoveRow() call completed without direct error.")
log.Printf("阶段 2: f2.RemoveRow() 调用成功完成 (未直接返回错误)。")
}
log.Printf("Phase 2: Attempting to save final output file '%s'...", finalOutputFile)
log.Printf("阶段 2: 尝试保存最终输出文件 '%s'...", finalOutputFile)
if err := f2.SaveAs(finalOutputFile); err != nil {
log.Printf("!!! Phase 2: Failed to save final output file '%s': %v", finalOutputFile, err)
log.Printf("!!! 阶段 2: 保存最终输出文件 '%s' 失败: %v", finalOutputFile, err)
} else {
log.Printf("Phase 2: Final output file '%s' saved.", finalOutputFile)
log.Printf("阶段 2: 最终输出文件 '%s' 已保存。", finalOutputFile)
log.Println(">>> Please manually inspect this final file with Excel as well.")
log.Println(">>> 请同时用 Excel 手动检查此最终文件。")
}
}()
log.Println("\n--- Test finished ---")
log.Println("\n--- 测试结束 ---")
log.Println(">>> Review console output for errors or panic stack traces.")
log.Println(">>> 请检查控制台输出的错误或 panic 堆栈跟踪。")
log.Println(">>> Manually check 'intermediate_corrupted_AB.xlsx' with Excel for corruption warnings.")
log.Println(">>> 请用 Excel 手动检查 'intermediate_corrupted_AB.xlsx' 文件是否有损坏警告。")
}
4. Run the Go Program / 运行 Go 程序:
go run excelize_bug_test.go
Describe the results you received
Phase 1 Output (intermediate_corrupted_AB.xlsx) / 阶段1输出 (intermediate_corrupted_AB.xlsx):
When intermediate_corrupted_AB.xlsx is opened with Microsoft Excel, a warning dialog appears: "Excel found a problem with some content in 'intermediate_corrupted_AB.xlsx'. Do you want us to try to recover as much as we can? If you trust the source of this workbook, click Yes."
当用 Microsoft Excel 打开 intermediate_corrupted_AB.xlsx 文件时,会弹出一个警告对话框:“Excel发现在 'intermediate_corrupted_AB.xlsx' 文件中存在一些内容问题。是否希望我们尽可能尝试恢复?如果您信任此工作簿的源,请单击‘是’。”
If "Yes" is clicked, Excel's recovery log (e.g., errorXXXXX_XX.xml) may indicate issues such as:
已删除的记录: /xl/calcChain.xml 部分的 公式 (计算属性) (Deleted record: Formula (Calculation properties) from /xl/calcChain.xml part)
如果单击“是”,Excel 的恢复日志(例如 errorXXXXX_XX.xml)可能会指出如下问题:
已删除的记录: /xl/calcChain.xml 部分的 公式 (计算属性)
Phase 2 Behavior (Go Program Console Output) / 阶段2行为 (Go程序控制台输出):
The program panics during the f2.RemoveRow() call in Phase 2.
程序在阶段2调用 f2.RemoveRow() 时发生 panic。
Panic Message / Panic 信息: panic: runtime error: invalid memory address or nil pointer dereference
Stack Trace includes / 堆栈跟踪包含:
[signal 0xc0000005 code=0x0 addr=0x0 pc=0x...]
goroutine 1 [running]:
github.com/xuri/excelize/v2.(*File).adjustCalcChain({0x...}, {0x...}, {0x...}, 0x1, 0x2, 0xffffffffffffffff, 0x...)
C:/Users/.../go/pkg/mod/github.com/xuri/excelize/v2@v2.9.0/adjust.go:850 +0x...
github.com/xuri/excelize/v2.init.func30({0x...}, ...)
C:/Users/.../go/pkg/mod/github.com/xuri/excelize/v2@v2.9.0/adjust.go:53 +0x...
github.com/xuri/excelize/v2.(*File).adjustHelper({0x...}, {0x...}, 0x1, 0x2, 0xffffffffffffffff)
C:/Users/.../go/pkg/mod/github.com/xuri/excelize/v2@v2.9.0/adjust.go:91 +0x...
github.com/xuri/excelize/v2.(*File).RemoveRow({0x...}, {0x...}, 0x2)
C:/Users/.../go/pkg/mod/github.com/xuri/excelize/v2@v2.9.0/rows.go:590 +0x...
main.main.func2()
.../excelize_bug_test.go:LINE_OF_F2_REMOVEROW +0x...
...
(Note: The adjust.go:850 refers to the line for i := 0; i < len(f.CalcChain.C); i++ { in excelize/v2@v2.9.0/adjust.go as loaded in my Go module cache, indicating f.CalcChain is likely nil.)
(注意:adjust.go:850 指的是在我 Go 模块缓存中加载的 excelize/v2@v2.9.0/adjust.go 文件中的 for i := 0; i < len(f.CalcChain.C); i++ { 这一行,表明 f.CalcChain 很可能为 nil。)
Describe the results you expected
Phase 1 / 阶段 1:
excelize should save intermediate_corrupted_AB.xlsx as a valid, non-corrupted Excel file. Microsoft Excel should open this file without any warnings or need for recovery.
excelize 应该将 intermediate_corrupted_AB.xlsx 保存为一个有效的、未损坏的 Excel 文件。Microsoft Excel 打开此文件时不应有任何警告或恢复提示。
Phase 2 / 阶段 2:
excelize should be able to successfully open the intermediate_corrupted_AB.xlsx file (which should not have been corrupted in the first place).
excelize 应该能够成功打开 intermediate_corrupted_AB.xlsx 文件(该文件首先就不应该被损坏)。
The subsequent f2.RemoveRow() call should execute without errors or panics.
后续的 f2.RemoveRow() 调用应该无错误或 panic 地执行。
The final_output_AB.xlsx file should be saved as a valid Excel file.
final_output_AB.xlsx 文件应该被保存为一个有效的 Excel 文件。
Go version
1.23.2
Excelize version or commit ID
2.9.0
Environment
Windows 11 24H2
Microsoft® Excel® 适用于 Microsoft 365MSO (版本 2504 Build 16.0.18730.20030) 64 位
Validations
- Check that there isn't already an issue that reports the same bug to avoid creating a duplicate.
- The provided reproduction is a minimal reproducible example of the bug.