前言:go 中 import 用于导入包

进入正题之前先介绍一个小知识点:

比如:

import "fmt"

在代码中就可以使用 fmt 包中的方法,如:

fmt.Println("hello world")

1、使用点操作引入包时,可以省略包前缀:

import . "fmt"

注意上面 fmt 前多了 . 字符。代码中使用时:

Println("hello world")

前缀 fmt 就不需要了。

2、别名操作可以给包起个小名儿。如:

import f "fmt"

f.Println("hello world")

3、_操作

由于 go 在引入包时调用包的 init 方法。所以使用_操作,主要是为了使用包的 init 函数,一般用在数据库方面的包中:

import (

"database/sql"

_ "github.com/ziutek/mymysql/godrv"

)

这个可以避免 go 编译时提示引入了包但未使用。


MYSQL 驱动的工作原理

SQL 驱动接口

Golang 的官方标准库提供了 database/sql,sql 包提供了保证 SQL 或类 SQL 数据库的泛用接口。使用 sql 包时必须注入(至少)一个数据库驱动。

Goals of the sql and sql/driver packages:

Provide a generic database API for a variety of SQL or SQL-like

databases. There currently exist Go libraries for SQLite, MySQL,

and Postgres, but all with a very different feel, and often
a non-Go-like feel.

image.png

  1. sql/driver 的目的是提供一个通用的数据库抽象接口,现在已经存在 SQLite,MySQL 等,但是他们的设计风格完全不同。

  2. 考虑到通用 SQL 的可移植的情况,而特定的 SQL 特性,和扩展应该被应用程序单独实现。

  3. 使用 sql/dirver 包,用户不需要考虑并发和线程安全的问题,通过 *sql.DB 可以在不同的 goroutines 之间共享,而不需要额外的同步。

MySQL 驱动

go-sql-dirver/mysql 是一个 MySQL 数据库的驱动, databases/sql 中定义的泛化接口的实现。

它需要自行安装:

go get -u github.com/go-sql-driver/mysql


import "database/sql"
import _ "github.com/go-sql-driver/mysql"

db, err := sql.Open("mysql", "user:password@/dbname")

导入 go-sql-dirver/mysql 包,这里用到前言中的最后一种导入包的情况,目的是运行 mysql 包中 init 函数,把 mysql 驱动注册到 database/sql 中,注册的名称是“mysql”,对象是一个实现了 Open 方法的结构体 MysqlSQLDirver。

// in mysql package
func init() {

	sql.Register("mysql", &MySQLDriver{})

}

// in sql package
func Register(name string, driver driver.Driver) {

	driversMu.Lock()
	defer driversMu.Unlock()
	if driver == nil {
		panic("sql: Register driver is nil")
	}
	if _, dup := drivers[name]; dup {
		panic("sql: Register called twice for driver " + name)
	}
	drivers[name] = driver
}

注册函数用到了 Sync 包中的同步工具。
同步的用途有两个,一个是避免多个线程在同一时刻操作同一个数据块,另一个是协调多个线程,以避免它们在同一时刻执行同一个代码块

在 Go 语言中,可供我们选择的同步工具并不少。其中,最重要且最常用的同步工具当属互斥量(mutual exclusion,简称 mutex。sync 包中的 Mutex 就是与其对应的类型,该类型的值可以被称为互斥量或者互斥锁。

每当线程或者 goroutine 进去临界区,抢占资源的时候就要进行 lock 操作,当离开临界区,unlock 操作。解锁最好用 defer 关键字。防止程序崩溃无法解锁的情况导致后续 goroutine 的阻塞。


// Open new Connection.
// See https://github.com/go-sql-driver/mysql#dsn-data-source-name for how
// the DSN string is formated
func (d MySQLDriver) Open(dsn string) (driver.Conn, error) {
	var err error

	// New mysqlConn
	mc := &mysqlConn{
		maxAllowedPacket: maxPacketSize,
		maxWriteSize:     maxPacketSize - 1,
	}
	mc.cfg, err = ParseDSN(dsn)
	if err != nil {
		return nil, err
	}
	mc.parseTime = mc.cfg.ParseTime
	mc.strict = mc.cfg.Strict

	// Connect to Server
	if dial, ok := dials[mc.cfg.Net]; ok {
		mc.netConn, err = dial(mc.cfg.Addr)
	} else {
		nd := net.Dialer{Timeout: mc.cfg.Timeout}
		mc.netConn, err = nd.Dial(mc.cfg.Net, mc.cfg.Addr)
	}
	if err != nil {
		return nil, err
	}

	// Enable TCP Keepalives on TCP connections
	if tc, ok := mc.netConn.(*net.TCPConn); ok {
		if err := tc.SetKeepAlive(true); err != nil {
			// Don't send COM_QUIT before handshake.
			mc.netConn.Close()
			mc.netConn = nil
			return nil, err
		}
	}

	mc.buf = newBuffer(mc.netConn)

	// Set I/O timeouts
	mc.buf.timeout = mc.cfg.ReadTimeout
	mc.writeTimeout = mc.cfg.WriteTimeout

	// Reading Handshake Initialization Packet
	cipher, err := mc.readInitPacket()
	if err != nil {
		mc.cleanup()
		return nil, err
	}

	// Send Client Authentication Packet
	if err = mc.writeAuthPacket(cipher); err != nil {
		mc.cleanup()
		return nil, err
	}

	// Handle response to auth packet, switch methods if possible
	if err = handleAuthResult(mc, cipher); err != nil {
		// Authentication failed and MySQL has already closed the connection
		// (https://dev.mysql.com/doc/internals/en/authentication-fails.html).
		// Do not send COM_QUIT, just cleanup and return the error.
		mc.cleanup()
		return nil, err
	}

	if mc.cfg.MaxAllowedPacket > 0 {
		mc.maxAllowedPacket = mc.cfg.MaxAllowedPacket
	} else {
		// Get max allowed packet size
		maxap, err := mc.getSystemVar("max_allowed_packet")
		if err != nil {
			mc.Close()
			return nil, err
		}
		mc.maxAllowedPacket = stringToInt(maxap) - 1
	}
	if mc.maxAllowedPacket < maxPacketSize {
		mc.maxWriteSize = mc.maxAllowedPacket
	}

	// Handle DSN Params
	err = mc.handleParams()
	if err != nil {
		mc.Close()
		return nil, err
	}

	return mc, nil
}

如果你尝试了以上的解决方案,还是发现偶尔会出现连接超时的错误的话,很可能是因为数据库本身对连接有一个超时时间的设置,如果超时时间到了数据库会单方面断掉连接,此时再用连接池内的连接进行访问就会出错,

自己看了一下 databas/sql 和 go-sql-driver 的源码,出现这个错误之后官方提供的方法是会进行两次的重连操作,如果两次重连都没有成功,就创建一个新的 SQL 链接,所以出现这个错误日志没什么影响,当然最好的做法是设置一下链接的空闲超时时间,设置的比 Mysql 服务器超时时间短就可以了,设置了这个之后就不会报这个错误了。当然不设也可以,设置了的话程序处理会更优雅一点!