引言
初学Go语言时,最让人困惑的代码之一莫过于:
go
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
)
那个孤独的下划线是什么意思?为什么要导入一个包却不用它?今天,让我们彻底理解这行代码背后的设计思想。
一、本质:一个全局map的故事
要理解这行代码,首先要明白:database/sql 包里有一个全局的map。
go
// database/sql 内部(简化版)
var drivers = make(map[string]driver.Driver)
这个map就是整个机制的核心------它是一个注册中心,等着别人往里面放东西。
二、注册:init函数的使命
MySQL驱动包的代码大概是这样的:
go
// github.com/go-sql-driver/mysql/driver.go
package mysql
import "database/sql"
type mysqlDriver struct{}
func init() {
// 往那个全局map里放东西!
sql.Register("mysql", &mysqlDriver{})
}
关键理解:
sql是包名,不是对象名Register是包的公开函数,不是方法调用- init函数在程序启动时自动执行,不是声明,是真的运行!
当程序启动时,init函数被执行,就相当于:
go
sql.drivers["mysql"] = &mysqlDriver{}
三、使用:从map里取
你的业务代码:
go
func main() {
db, _ := sql.Open("mysql", "user:pass@/dbname")
}
sql.Open 的实现大致是:
go
func Open(driverName, dataSourceName string) (*DB, error) {
driver := drivers[driverName] // 从map里取
return driver.Open(dataSourceName)
}
所以整个过程就是:
- 程序启动:MySQL驱动往map里放自己的实现
- 运行时:你通过名字从map里取出实现来用
四、为什么用下划线?
回到最初的问题:为什么要用 _?
go
import _ "github.com/go-sql-driver/mysql"
下划线表示匿名导入,意思是:
- 我导入这个包,只是为了执行它的init函数
- 我不需要直接使用这个包里的任何标识符
就像你去餐厅:
- 你只需要服务员(
database/sql)服务 - 但后厨师傅(MySQL驱动)必须先来报到
- 你不用直接和师傅说话,只需要他来登个记
五、面向接口编程的精髓
这个机制完美体现了面向接口编程的几个核心原则:
1. 依赖倒置
go
// 高层次模块(你的代码)依赖抽象
import "database/sql"
// 低层次模块(MySQL驱动)也依赖抽象
import "database/sql" // 驱动也依赖同一个接口
// 抽象(database/sql)不依赖细节
// 细节(MySQL驱动)依赖抽象
2. 开闭原则
go
// 新增一个PostgreSQL驱动,只需:
import _ "github.com/lib/pq"
// 完全不用改database/sql的代码
// 也不用改你的业务代码
db, _ := sql.Open("postgres", "xxx")
3. 插件架构
go
// 这就是一个完整的插件系统:
// - 内核:database/sql(定义接口+注册中心)
// - 插件:各种驱动(实现接口+注册自己)
// - 使用者:你的代码(通过名字调用)
var drivers = make(map[string]Driver) // 注册中心
func Register(name string, driver Driver) // 插件接口
func Open(name string) DB // 使用接口
六、实战:自己实现一个类似系统
为了加深理解,我们来写一个迷你版的"日志插件系统":
go
// 1. 定义接口和注册中心(类似database/sql)
package logger
type Logger interface {
Log(msg string)
}
var loggers = make(map[string]Logger)
func Register(name string, l Logger) {
loggers[name] = l // 往全局map里放
}
func Get(name string) Logger {
return loggers[name] // 从全局map里取
}
// 2. 文件日志插件(类似mysql驱动)
package filelog
import "logger"
type FileLogger struct{}
func (f FileLogger) Log(msg string) {
// 写入文件...
}
func init() {
// 注册自己!
logger.Register("file", &FileLogger{})
}
// 3. 控制台日志插件
package consolelog
import "logger"
type ConsoleLogger struct{}
func (c ConsoleLogger) Log(msg string) {
fmt.Println(msg)
}
func init() {
logger.Register("console", &ConsoleLogger{})
}
// 4. 你的程序
package main
import (
"logger"
_ "filelog" // 匿名导入,触发注册
_ "consolelog" // 另一个插件
)
func main() {
// 根据配置选择不同的实现
log := logger.Get("file")
log.Log("hello") // 写入文件
log = logger.Get("console")
log.Log("world") // 打印到控制台
}
七、总结
_ "github.com/go-sql-driver/mysql" 这行代码,本质上是:
| 概念 | 本质 |
|---|---|
| 下划线 | 我只要init,不要其他 |
| init函数 | 程序启动时自动执行的注册动作 |
| sql.Register | 往全局map里放键值对 |
| 全局map | 唯一的注册中心 |
| sql.Open | 从map里取实现来用 |
一句话总结:
这行代码就是让MySQL驱动在程序启动时,主动往
database/sql的全局注册表里写一条记录:"我的名字叫mysql,这是我的实现,以后有人用mysql就找我"。
理解了这行代码,你就理解了:
- Go语言的init机制
- 包导入的副作用
- 面向接口编程
- 插件架构模式
- 依赖倒置原则
一个下划线,道尽Go语言最精巧的设计哲学。