有时候,最好的创意来自于最疯狂的想法。比如,把 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 接口的力量。也许有一天,我们会看到更多"非传统"的数据源被抽象成数据库驱动。
毕竟,在编程世界里,只要有接口,一切皆有可能。
往期部分文章列表
- 穿墙术大揭秘:用 Go 手搓一个"内网穿透"神器!
- 布隆过滤器(go):一个可能犯错但从不撒谎的内存大师
- 自由通讯的魔法:Go从零实现UDP/P2P 聊天工具
- Go语言实现的简易远程传屏工具:让你的屏幕「飞」起来
- 当你的程序学会了"诈尸":Go 实现 Windows 进程守护术
- 验证码识别API:告别收费接口,迎接免费午餐
- 用 Go 给 Windows 装个"顺风耳":两分钟写个录音小工具
- 无奈!我用go写了个MySQL服务
- 使用 Go + govcl 实现 Windows 资源管理器快捷方式管理器
- 用 Go 手搓一个 NTP 服务:从"时间混乱"到"精准同步"的奇幻之旅
- 用 Go 手搓一个 Java 构建工具:当 IDE 不在身边时的自救指南
- 深入理解 Windows 全局键盘钩子(Hook):拦截 Win 键的 Go 实现
- 用 Go 语言实现《周易》大衍筮法起卦程序
- Go 语言400行代码实现 INI 配置文件解析器:支持注释、转义与类型推断
- 高性能 Go 语言带 TTL 的内存缓存实现:精确过期、自动刷新、并发安全
- Golang + OpenSSL 实现 TLS 安全通信:从私有 CA 到动态证书加载