Golang和MySQL(或PostgreSQL)的使用教程

481 阅读6分钟

让我们使用Go的一个标准库中的工具包:database/sql 。它是一个抽象,你也需要导入一个数据库驱动。我们将尽可能的简单和 "落到实处"。我不是ORM的粉丝,所以这应该很有趣。无论你使用什么SQL数据库,代码都应该是类似或相同的。阅读我关于实际数据库本身的快速入门帖子,MySQLPostgreSQL都可以开始使用。

快速入门

导入驱动程序

导入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 此外,只有在调用CommitRollback() 后,才会返回连接。如果你忘了完全迭代行,也忘了关闭它,那么你的连接将永远不会回到池中。

此外,保持一个连接空闲很长时间会导致问题。如果你因为一个连接空闲时间过长而出现连接超时,可以尝试设置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
}

参考资料