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

有时候,最好的创意来自于最疯狂的想法。比如,把 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 接口的力量。也许有一天,我们会看到更多"非传统"的数据源被抽象成数据库驱动。

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

往期部分文章列表

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容