用 Go 接口把 Excel 变成数据库:一个疯狂但可行的想法

446 阅读8分钟

有时候,最好的创意来自于最疯狂的想法。比如,把 Excel 当数据库用。

前言:一个"疯狂"的想法

有一天,我突发奇想:Go 的 database/sql 包通过一系列接口定义了数据库驱动的标准,只要实现了这些接口,任何数据源都可以当作数据库来使用。那么问题来了——能不能把 Excel 当作数据库?

听起来很疯狂对吧?但仔细想想,Excel 本身就是结构化数据,有行有列,有表头有数据,这不就是一张表吗?于是,我决定动手试试。

Go 接口:让不可能变成可能

接口的力量

Go 的接口设计是这整个想法的核心。让我们看看 database/sql/driver 包定义的核心接口:

type Driver interface {
    Open(name string) (Conn, error)
}

type Conn interface {
    Prepare(query string) (Stmt, error)
    Close() error
    Begin() (Tx, error)
}

type Stmt interface {
    Close() error
    NumInput() int
    Exec(args []Value) (Result, error)
    Query(args []Value) (Rows, error)
}

type Rows interface {
    Columns() []string
    Close() error
    Next(dest []Value) error
}

看到了吗?Go 只关心行为,不关心具体实现。这意味着:

  • 数据库可以是 MySQL、PostgreSQL
  • 也可以是 CSV 文件、JSON 文件
  • 甚至可以是 Excel 文件

接口的优势分析

1. 抽象与解耦

// 用户代码只需要关心 SQL,不需要知道背后是啥
db, _ := sql.Open("excel", "./data.xlsx")
rows, _ := db.Query("SELECT name, age FROM Users")

用户代码完全不需要知道背后是 Excel 还是 MySQL,这就是接口的魅力。

2. 插件化架构

sql.Register("excel", &ExcelDriver{})
sql.Register("csv", &CSVDriver{})
sql.Register("json", &JSONDriver{})

任何实现了标准接口的驱动都可以无缝集成。

3. 测试友好

// 可以轻松创建 mock 实现
type MockDriver struct{}
// 实现相同接口,返回预设数据

实现 Excel 数据库驱动

核心设计思路

既然决定要实现,那就来真的。我设计了这样的结构:

  • 一个 Excel 文件 = 一个数据库
  • 每个工作表 = 一张表
  • 第一行 = 列名
  • 其余行 = 数据

这样设计更符合 Excel 的自然使用方式。

关键实现代码

// ExcelDriver 实现 driver.Driver 接口
type ExcelDriver struct{}

func (d *ExcelDriver) Open(name string) (driver.Conn, error) {
    return &ExcelConn{filePath: name}, nil
}

// ExcelConn 实现 driver.Conn 接口
type ExcelConn struct {
    filePath string
    file     *excelize.File
}

func (c *ExcelConn) Prepare(query string) (driver.Stmt, error) {
    return &ExcelStmt{conn: c, query: query}, nil
}

查询解析

为了让 Excel "理解" SQL,我们需要解析查询:

// 简单解析 SELECT * FROM table
re := regexp.MustCompile(`SELECT\s+(.+)\s+FROM\s+(\w+)`)
matches := re.FindStringSubmatch(strings.TrimSpace(query))

虽然功能有限,但足以支持基本查询。

实际应用演示

// 注册驱动
sql.Register("excel", &ExcelDriver{})

// 连接 Excel 文件(就像连接数据库一样)
db, _ := sql.Open("excel", "./sample.xlsx")

// 执行查询
rows, _ := db.Query("SELECT name, age FROM Users")
for rows.Next() {
    var name, age string
    rows.Scan(&name, &age)
    fmt.Printf("Name: %s, Age: %s\n", name, age)
}

看,完全一样的 API!

为什么 Go 接口这么棒?

1. 鸭子类型哲学

"如果它走起来像鸭子,叫起来像鸭子,那它就是鸭子。"

Go 接口完美体现了这一点。只要你的类型实现了接口的方法,它就可以当作接口类型使用。

2. 隐式实现

// 不需要显式声明实现关系
type MyType struct{}

// 只要实现了接口方法,就自动实现了接口
func (m MyType) SomeMethod() {}

这比 Java 的 implements 更灵活。

3. 组合优于继承

Go 没有继承,但通过接口组合可以实现强大的功能扩展。

4. 标准库的统一性

无论底层是 MySQL、PostgreSQL 还是我们的 Excel 驱动,上层 API 完全一致。

项目意义与启发

技术价值

  • 展示了 Go 接口的强大能力
  • 提供了数据访问的另一种思路
  • 证明了接口抽象的通用性

设计哲学

  • 关注行为而非实现
  • 标准接口,多样实现
  • 小接口,强组合

全部源码

package main

import (
	"database/sql"
	"database/sql/driver"
	"fmt"
	"io"
	"regexp"
	"strings"

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

// ExcelDriver 实现 driver.Driver 接口
type ExcelDriver struct{}

// Open 打开一个 Excel 文件作为数据库
func (d *ExcelDriver) Open(name string) (driver.Conn, error) {
	return &ExcelConn{filePath: name}, nil
}

// ExcelConn 实现 driver.Conn 接口
type ExcelConn struct {
	filePath string
	file     *excelize.File
}

func (c *ExcelConn) Prepare(query string) (driver.Stmt, error) {
	return &ExcelStmt{conn: c, query: query}, nil
}

func (c *ExcelConn) Close() error {
	if c.file != nil {
		c.file.Close()
	}
	return nil
}

func (c *ExcelConn) Begin() (driver.Tx, error) {
	return nil, fmt.Errorf("transactions not supported")
}

// ExcelStmt 实现 driver.Stmt 接口
type ExcelStmt struct {
	conn  *ExcelConn
	query string
}

func (s *ExcelStmt) Close() error {
	return nil
}

func (s *ExcelStmt) NumInput() int {
	return -1 // 不限制参数数量
}

func (s *ExcelStmt) Exec(args []driver.Value) (driver.Result, error) {
	return nil, fmt.Errorf("exec not supported")
}

func (s *ExcelStmt) Query(args []driver.Value) (driver.Rows, error) {
	return parseAndExecuteQuery(s.conn, s.query, args)
}

// ExcelRows 实现 driver.Rows 接口
type ExcelRows struct {
	columns []string
	data    [][]string
	current int
}

func (r *ExcelRows) Columns() []string {
	return r.columns
}

func (r *ExcelRows) Close() error {
	return nil
}

func (r *ExcelRows) Next(dest []driver.Value) error {
	if r.current >= len(r.data) {
		return io.EOF
	}

	for i, v := range r.data[r.current] {
		if i < len(dest) {
			dest[i] = v
		}
	}
	r.current++
	return nil
}

// 解析并执行查询
func parseAndExecuteQuery(conn *ExcelConn, query string, args []driver.Value) (driver.Rows, error) {
	// 简单解析 SELECT * FROM table
	re := regexp.MustCompile(`SELECT\s+(.+)\s+FROM\s+(\w+)`)
	matches := re.FindStringSubmatch(strings.TrimSpace(query))
	if len(matches) != 3 {
		return nil, fmt.Errorf("unsupported query: %s", query)
	}

	selectFields := strings.TrimSpace(matches[1])
	tableName := strings.TrimSpace(matches[2])

	// 打开 Excel 文件(如果还没有打开的话)
	if conn.file == nil {
		f, err := excelize.OpenFile(conn.filePath)
		if err != nil {
			return nil, fmt.Errorf("failed to open Excel file: %v", err)
		}
		conn.file = f
	}

	// 检查工作表是否存在
	allSheets := conn.file.GetSheetMap()
	sheetExists := false
	sheetName := ""

	for sheetNum, name := range allSheets {
		if strings.EqualFold(name, tableName) {
			sheetExists = true
			sheetName = name
			break
		}
		// 也检查数字形式的 sheet name
		if fmt.Sprintf("%d", sheetNum) == tableName {
			sheetExists = true
			sheetName = name
			break
		}
	}

	if !sheetExists {
		return nil, fmt.Errorf("table (sheet) %s not found in Excel file", tableName)
	}

	// 获取工作表的所有行
	rows, err := conn.file.GetRows(sheetName)
	if err != nil {
		return nil, fmt.Errorf("failed to read sheet %s: %v", sheetName, err)
	}

	if len(rows) == 0 {
		return &ExcelRows{columns: []string{}, data: [][]string{}, current: 0}, nil
	}

	// 第一行作为列名
	headers := rows[0]
	selectedColumns := headers

	if selectFields != "*" {
		selectedColumns = strings.Split(selectFields, ",")
		for i := range selectedColumns {
			selectedColumns[i] = strings.TrimSpace(selectedColumns[i])
		}
	}

	// 构建结果数据
	var resultData [][]string
	for i := 1; i < len(rows); i++ {
		row := rows[i]
		selectedRow := make([]string, len(selectedColumns))

		for j, col := range selectedColumns {
			// 找到列索引
			colIndex := -1
			for k, header := range headers {
				if strings.TrimSpace(header) == col {
					colIndex = k
					break
				}
			}

			if colIndex >= 0 && colIndex < len(row) {
				selectedRow[j] = row[colIndex]
			} else {
				selectedRow[j] = ""
			}
		}
		resultData = append(resultData, selectedRow)
	}

	return &ExcelRows{
		columns: selectedColumns,
		data:    resultData,
		current: 0,
	}, nil
}

func main() {
	// 注册驱动
	sql.Register("excel", &ExcelDriver{})

	// 创建示例 Excel 文件
	createSampleExcel()

	// 连接数据库(实际上是 Excel 文件)
	db, err := sql.Open("excel", "./sample.xlsx")
	if err != nil {
		fmt.Println("Error opening database:", err)
		return
	}
	defer db.Close()

	// 执行查询 - 从 Users 工作表查询
	fmt.Println("=== Querying Users sheet ===")
	rows, err := db.Query("SELECT name, age FROM Users")
	if err != nil {
		fmt.Println("Error executing query:", err)
		return
	}
	defer rows.Close()

	// 获取列名
	columns, _ := rows.Columns()
	fmt.Println("Columns:", columns)

	// 遍历结果
	for rows.Next() {
		var name, age string
		err := rows.Scan(&name, &age)
		if err != nil {
			fmt.Println("Error scanning row:", err)
			continue
		}
		fmt.Printf("Name: %s, Age: %s\n", name, age)
	}

	// 查询 Products 工作表
	fmt.Println("\n=== Querying Products sheet ===")
	rows2, err := db.Query("SELECT product_name, price FROM Products")
	if err != nil {
		fmt.Println("Error executing query:", err)
		return
	}
	defer rows2.Close()

	// 获取列名
	columns2, _ := rows2.Columns()
	fmt.Println("Columns:", columns2)

	// 遍历结果
	for rows2.Next() {
		var productName, price string
		err := rows2.Scan(&productName, &price)
		if err != nil {
			fmt.Println("Error scanning row:", err)
			continue
		}
		fmt.Printf("Product: %s, Price: %s\n", productName, price)
	}

	// 查询所有列
	fmt.Println("\n=== Querying all columns from Users ===")
	rows3, err := db.Query("SELECT * FROM Users")
	if err != nil {
		fmt.Println("Error executing query:", err)
		return
	}
	defer rows3.Close()

	// 获取列名
	columns3, _ := rows3.Columns()
	fmt.Println("Columns:", columns3)

	// 遍历结果
	for rows3.Next() {
		values := make([]interface{}, len(columns3))
		valuePtrs := make([]interface{}, len(columns3))
		for i := range values {
			valuePtrs[i] = &values[i]
		}

		err := rows3.Scan(valuePtrs...)
		if err != nil {
			fmt.Println("Error scanning row:", err)
			continue
		}

		for i, v := range values {
			fmt.Printf("%s: %v ", columns3[i], v)
		}
		fmt.Println()
	}
}

// 创建示例 Excel 文件
func createSampleExcel() {
	f := excelize.NewFile()

	// 删除默认工作表
	f.DeleteSheet("Sheet1")

	// 创建 Users 工作表
	usersSheet := "Users"
	f.NewSheet(usersSheet)

	// 添加表头
	f.SetCellValue(usersSheet, "A1", "name")
	f.SetCellValue(usersSheet, "B1", "age")
	f.SetCellValue(usersSheet, "C1", "city")

	// 添加数据
	f.SetCellValue(usersSheet, "A2", "毛一一")
	f.SetCellValue(usersSheet, "B2", "25")
	f.SetCellValue(usersSheet, "C2", "江西九江")

	f.SetCellValue(usersSheet, "A3", "孙二二")
	f.SetCellValue(usersSheet, "B3", "30")
	f.SetCellValue(usersSheet, "C3", "北京")

	f.SetCellValue(usersSheet, "A4", "周三三")
	f.SetCellValue(usersSheet, "B4", "35")
	f.SetCellValue(usersSheet, "C4", "山东烟台")

	// 创建 Products 工作表
	productsSheet := "Products"
	f.NewSheet(productsSheet)

	// 添加表头
	f.SetCellValue(productsSheet, "A1", "product_name")
	f.SetCellValue(productsSheet, "B1", "price")
	f.SetCellValue(productsSheet, "C1", "category")

	// 添加数据
	f.SetCellValue(productsSheet, "A2", "平板")
	f.SetCellValue(productsSheet, "B2", "999.99")
	f.SetCellValue(productsSheet, "C2", "电子产品")

	f.SetCellValue(productsSheet, "A3", "书藉")
	f.SetCellValue(productsSheet, "B3", "19.99")
	f.SetCellValue(productsSheet, "C3", "学习资料")

	f.SetCellValue(productsSheet, "A4", "手机")
	f.SetCellValue(productsSheet, "B4", "699.00")
	f.SetCellValue(productsSheet, "C4", "电子产品")

	// 保存文件
	f.SaveAs("./sample.xlsx")
}

结语:从疯狂想法到现实

把 Excel 当数据库,听起来确实疯狂,但通过 Go 的接口机制,这个想法变成了现实。这正是 Go 语言设计哲学的体现:简单、灵活、强大。

当然,这个 Excel 驱动还有很多限制:

  • 不支持事务
  • SQL 功能有限
  • 性能不如真正的数据库

但作为一个概念验证,它完美展示了 Go 接口的力量。也许有一天,我们会看到更多"非传统"的数据源被抽象成数据库驱动。

毕竟,在编程世界里,只要有接口,一切皆有可能。

往期部分文章列表