让我们使用Go的一个标准库中的工具包:database/sql 。它是一个抽象,你也需要导入一个数据库驱动。我们将尽可能的简单和 "落到实处"。我不是ORM的粉丝,所以这应该很有趣。无论你使用什么SQL数据库,代码都应该是类似或相同的。阅读我关于实际数据库本身的快速入门帖子,MySQL和PostgreSQL都可以开始使用。
快速入门
导入驱动程序
导入database/sql 和数据库的驱动程序。不要直接使用驱动程序--如果可能的话,只参考包中定义的类型。这是为了避免依赖性,这样你以后就可以很容易地改变驱动或数据库。
import_ 中的下划线意味着我们只是在运行包的init() 函数。没有导出的包将是可见的。在内部,驱动程序将自己注册为可用于database/sql 包。
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
)
访问数据库
func main() {
// [user[:pass]@][protocol[(addr)]]/dbname[?p1=v1&...]
db, err := sql.Open("mysql", "user:pass@/dbname")
if err != nil {
log.Fatal(err)
}
defer db.Close()
err = db.Ping()
if err != nil {
log.Fatal(err.Error())
}
}
请注意,sql.DB 不是一个连接,而是一个代表底层连接池的抽象概念。当你使用Open() ,你会得到一个数据库的句柄,而连接在你需要时才会打开。这意味着,如果服务器不可用或用户/密码无效,它不会返回错误。如果你想在进行查询之前进行检查,可以使用db.Ping()
- 这不会立即建立连接--第一个连接将在需要时懒散地建立。
- 记住要经常检查和处理所有操作中返回的错误。
连接池
需要注意的是,sql.DB 对象被设计成长期使用的,所以不要频繁地打开和关闭数据库。为你需要访问的每个不同的数据库创建一个 sql.DB 对象,并保持其开放,直到程序完成。根据需要传递它,或者让它在全球范围内可用,但要确保你保持它的开放。如果你不这样做,你会遇到连接问题或零星的故障。
连接问题。你是否已经没有数据库连接了?
如果你遇到 "连接太多 "的问题,你可能需要设置func (*DB) SetMaxOpenConns来防止无限的连接数。这限制了数据库的总开放连接数。
如果你执行一个类似于db.Query("SELECT * FROM table") 的查询,一个连接就会被打开,并且不会返回,直到你明确地Close() 这个连接或者用Next() 遍历记录。sql.Tx 此外,只有在调用Commit 或Rollback() 后,才会返回连接。如果你忘了完全迭代行,也忘了关闭它,那么你的连接将永远不会回到池中。
此外,保持一个连接空闲很长时间会导致问题。如果你因为一个连接空闲时间过长而出现连接超时,可以尝试设置db.SetMaxIdleConns(0) 。然而,如果你得到了延迟,你可以增加它。这个设置将不得不取决于你的使用情况。
检索结果集
从数据存储中检索结果的简便方法。
- 执行一个返回行的查询。
- 准备一个重复使用的语句。多次执行它。销毁它。
- 以一次性的方式执行一个语句,不需要准备语句。
- 执行一个返回单行的查询(我们在这里使用快捷键func)。
如果一个函数名称包含Query ,那么它的目的是向数据库提出问题并返回一组行。不返回行的语句必须使用Exec() 函数。
var (
id int
name string
)
rows, err := db.Query("SELECT id, name from users where id = ?", 1)
if err != nil {
log.Fatal(err)
}
defer rows.Close() // Important to close! This is no-op if already closed
// Iterate rows. Make sure you don't skip/exit the loop early
// or the connection remains open. Don't defer inside loop also.
for rows.Next() {
err := rows.Scan(&id, &name)
if err != nil {
log.Fatal(err)
}
log.Println(id, name)
}
err = rows.Err() // check at end of the loop
if err != nil {
log.Fatal(err)
}
当你对行进行迭代并将其扫描到变量中时,Go会为你转换类型如果你有一列是字符串类型,即:varchar ,但你知道它们是ints,只要在你的Scan() 中传入一个int ,Go就会调用strconv.ParseInt() 。这就是Go的习惯做法。厉害吧!
如果你的查询最多返回一条记录,你可以使用一个快捷方式。
var name string
err = db.QueryRow("SELECT name from users where id = ?" 1).Scan(&name)
if err != nil {
log.Fatal(err)
}
fmt.Println(name)
修改数据
使用Exec() 到INSERT,UPDATE,DELETE 。不要使用db.Query 来执行这些语句,否则连接将永远不会被释放。
res, err := db.Exec("DELETE FROM users LIMIT 1")
if err != nil {
log.Fatal(err)
}
rowCount, err := res.RowsAffected()
if err != nil {
log.Fatal(err)
}
log.Println(rowCount)
使用预处理语句
stmt, err := db.Prepare("INSERT INTO squareNum VALUES( ?, ? )")
if err != nil {
panic(err.Error()) // don't do this! just an example
}
defer stmt.Close()
for i := 0; i < 25; i++ {
_, err = stmt.Exec(i, (i * i)) // insert tuples
if err != nil {
panel(err.Error())
}
}
空值
Nulls会导致很多难看的代码,所以如果你能避免它们--就这么做。**你将不得不使用包中的一个特殊类型来处理它们。比如说。
for rows.Next() {
var s sql.NullString
err := rows.Scan(&s)
if s.Valid { } else { }
}
请注意,在数据库层有另一个解决方法来处理NULLs。
rows, err := db.Query(`
SELECT name, COALESCE(other_field, '') as other_field WHERE id = ?
`, 42)
for rows.Next() {
err := rows.Scan(&name, &otherField)
// If `other_field` was NULL, `otherField` is now an empty string. This works with other data types as well.
}
在go-sql-driver wiki中提到了另一个解决方法。
缺点
- 不要在循环中延迟。
- 除非你创建一个
Nullxxxx类型,否则你不能将一个NULL扫描到一个变量中。最好的办法是避免数据库中出现NULL。 - 如果你想释放你的连接,请记住总是做
rows.Close()。 - 千万不要在非查询语句中使用
db.Query(),因为你会泄露连接。 - 总是处理
rows.Next(),因为它可能会使循环出现异常中断。 - 如果一个语句只使用一次,不要使用准备好的语句,因为它将做两次往返。
- 如果一个代码是并发的,避免使用准备好的语句,因为它们会在不同的连接上运行多次。
.Scan()隐含地为你转换数值,所以你不需要再写 。strconv- 你的下一个语句可能不会使用相同的连接,所以避免在SQL中写BEGIN或LOCK。使用
sql.Tx来代替。
完整的例子
请看这个来自thewhitetulip.gitbooks.io的例子。另外,这里是作者参考的完整项目。
package main
import (
"database/sql"
"fmt"
"log"
"net/http"
"time"
_ "github.com/mattn/go-sqlite3"
)
var database *sql.DB
var err error
//Task is the struct used to identify tasks
type Task struct {
Id int
Title string
Content string
Created string
}
//Context is the struct passed to templates
type Context struct {
Tasks []Task
Navigation string
Search string
Message string
}
func init() {
database, err = sql.Open("sqlite3", "./tasks.db")
if err != nil {
fmt.Println(err)
}
}
func main() {
http.HandleFunc("/", ShowAllTasksFunc)
http.HandleFunc("/add/", AddTaskFunc)
fmt.Println("running on 8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
//ShowAllTasksFunc is used to handle the "/" URL which is the default one
func ShowAllTasksFunc(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
context := GetTasks() //true when you want non deleted notes
w.Write([]byte(context.Tasks[0].Title))
} else {
http.Redirect(w, r, "/", http.StatusFound)
}
}
func GetTasks() Context {
var task []Task
var context Context
var TaskID int
var TaskTitle string
var TaskContent string
var TaskCreated time.Time
var getTasksql string
getTasksql = "select id, title, content, created_date from task;"
rows, err := database.Query(getTasksql)
if err != nil {
fmt.Println(err)
}
defer rows.Close()
for rows.Next() {
err := rows.Scan(&TaskID, &TaskTitle, &TaskContent, &TaskCreated)
if err != nil {
fmt.Println(err)
}
TaskCreated = TaskCreated.Local()
a := Task{Id: TaskID, Title: TaskTitle, Content: TaskContent,
Created: TaskCreated.Format(time.UnixDate)[0:20]}
task = append(task, a)
}
context = Context{Tasks: task}
return context
}
//AddTaskFunc is used to handle the addition of new task, "/add" URL
func AddTaskFunc(w http.ResponseWriter, r *http.Request) {
title := "random title"
content := "random content"
truth := AddTask(title, content)
if truth != nil {
log.Fatal("Error adding task")
}
w.Write([]byte("Added task"))
}
//AddTask is used to add the task in the database
func AddTask(title, content string) error {
query:="insert into task(title, content, created_date, last_modified_at)\
values(?,?,datetime(), datetime())"
restoreSQL, err := database.Prepare(query)
if err != nil {
fmt.Println(err)
}
tx, err := database.Begin()
_, err = tx.Stmt(restoreSQL).Exec(title, content)
if err != nil {
fmt.Println(err)
tx.Rollback()
} else {
log.Print("insert successful")
tx.Commit()
}
return err
}