教程:訪問關係型資料庫

本教程介紹使用 Go 及其標準庫中的 database/sql 包訪問關係型資料庫的基礎知識。

如果您對 Go 及其工具鏈有基本瞭解,將能更好地利用本教程。如果您是第一次接觸 Go,請參閱教程:開始使用 Go 進行快速入門。

您將使用的 database/sql 包包含用於連線資料庫、執行事務、取消正在進行的操作等的型別和函式。有關使用此包的更多詳細資訊,請參閱訪問資料庫

在本教程中,您將建立一個數據庫,然後編寫程式碼來訪問該資料庫。您的示例專案將是一個關於老式爵士樂唱片的資料儲存庫。

在本教程中,您將按以下章節進行學習:

  1. 為您的程式碼建立一個資料夾。
  2. 設定資料庫。
  3. 匯入資料庫驅動程式。
  4. 獲取資料庫控制代碼並連線。
  5. 查詢多行資料。
  6. 查詢單行資料。
  7. 新增資料。

注意:有關其他教程,請參閱教程

前提條件

為您的程式碼建立一個資料夾

首先,為您的程式碼建立一個資料夾。

  1. 開啟命令列提示符並切換到您的主目錄。

    在 Linux 或 Mac 上

    $ cd
    

    在 Windows 上

    C:\> cd %HOMEPATH%
    

    在本教程的其餘部分,我們將使用 $ 作為提示符。我們使用的命令在 Windows 上也能執行。

  2. 在命令列提示符下,為您的程式碼建立一個名為 data-access 的目錄。

    $ mkdir data-access
    $ cd data-access
    
  3. 建立一個模組,以便管理您在本教程中新增的依賴項。

    執行 go mod init 命令,並指定您的新程式碼的模組路徑。

    $ go mod init example/data-access
    go: creating new go.mod: module example/data-access
    

    此命令會建立一個 go.mod 檔案,其中將列出您新增的依賴項以便跟蹤。更多資訊,請務必參閱管理依賴項

    注意:在實際開發中,您會指定一個更符合您自身需求的模組路徑。更多資訊,請參閱管理依賴項

接下來,您將建立一個數據庫。

設定資料庫

在此步驟中,您將建立要使用的資料庫。您將使用 DBMS 本身的 CLI 來建立資料庫和表,以及新增資料。

您將建立一個包含老式黑膠爵士樂唱片資料的資料庫。

這裡的程式碼使用 MySQL CLI,但大多數 DBMS 都有自己的類似功能的 CLI。

  1. 開啟一個新的命令列提示符。

  2. 在命令列中,登入您的 DBMS,如下面的 MySQL 示例所示。

    $ mysql -u root -p
    Enter password:
    
    mysql>
    
  3. mysql 命令列提示符下,建立一個數據庫。

    mysql> create database recordings;
    
  4. 切換到您剛建立的資料庫,以便可以新增表。

    mysql> use recordings;
    Database changed
    
  5. 在您的文字編輯器中,在 data-access 資料夾中,建立一個名為 create-tables.sql 的檔案,用於存放新增表的 SQL 指令碼。

  6. 將以下 SQL 程式碼貼上到檔案中,然後儲存檔案。

    DROP TABLE IF EXISTS album;
    CREATE TABLE album (
      id         INT AUTO_INCREMENT NOT NULL,
      title      VARCHAR(128) NOT NULL,
      artist     VARCHAR(255) NOT NULL,
      price      DECIMAL(5,2) NOT NULL,
      PRIMARY KEY (`id`)
    );
    
    INSERT INTO album
      (title, artist, price)
    VALUES
      ('Blue Train', 'John Coltrane', 56.99),
      ('Giant Steps', 'John Coltrane', 63.99),
      ('Jeru', 'Gerry Mulligan', 17.99),
      ('Sarah Vaughan', 'Sarah Vaughan', 34.98);
    

    在此 SQL 程式碼中,您將執行以下操作:

    • 刪除 (drop) 一個名為 album 的表。先執行此命令可以方便您以後重新執行指令碼,如果想從頭開始的話。

    • 建立一個名為 album 的表,包含四列:titleartistprice。每行的 id 值由 DBMS 自動建立。

    • 新增四行帶有值的資料。

  7. mysql 命令列提示符下,執行您剛建立的指令碼。

    您將使用以下形式的 source 命令:

    mysql> source /path/to/create-tables.sql
    
  8. 在您的 DBMS 命令列提示符下,使用 SELECT 語句驗證您是否成功建立了帶有資料的表。

    mysql> select * from album;
    +----+---------------+----------------+-------+
    | id | title         | artist         | price |
    +----+---------------+----------------+-------+
    |  1 | Blue Train    | John Coltrane  | 56.99 |
    |  2 | Giant Steps   | John Coltrane  | 63.99 |
    |  3 | Jeru          | Gerry Mulligan | 17.99 |
    |  4 | Sarah Vaughan | Sarah Vaughan  | 34.98 |
    +----+---------------+----------------+-------+
    4 rows in set (0.00 sec)
    

接下來,您將編寫一些 Go 程式碼來連線,以便可以進行查詢。

查詢並匯入資料庫驅動程式

現在您已經有了帶有資料的資料庫,開始編寫您的 Go 程式碼吧。

找到並匯入一個數據庫驅動程式,它將把您透過 database/sql 包中的函式發出的請求轉換為資料庫能理解的請求。

  1. 在您的瀏覽器中,訪問 SQLDrivers wiki 頁面,以確定可以使用哪個驅動程式。

    使用頁面上的列表確定您將使用的驅動程式。在本教程中,要訪問 MySQL,您將使用 Go-MySQL-Driver

  2. 請注意驅動程式的包名——此處為 github.com/go-sql-driver/mysql

  3. 使用您的文字編輯器,建立一個檔案來編寫您的 Go 程式碼,並將檔案儲存為 main.go 在您之前建立的 data-access 目錄中。

  4. 將以下程式碼貼上到 main.go 中,以匯入驅動程式包。

    package main
    
    import "github.com/go-sql-driver/mysql"
    

    在此程式碼中,您將執行以下操作:

    • 將您的程式碼新增到 main 包中,以便可以獨立執行它。

    • 匯入 MySQL 驅動程式 github.com/go-sql-driver/mysql

匯入驅動程式後,您將開始編寫程式碼以訪問資料庫。

獲取資料庫控制代碼並連線

現在編寫一些 Go 程式碼,使用資料庫控制代碼讓您可以訪問資料庫。

您將使用指向 sql.DB 結構體的指標,它表示對特定資料庫的訪問。

編寫程式碼

  1. 在 main.go 中,緊接著您剛才新增的 import 程式碼之後,貼上以下 Go 程式碼來建立一個數據庫控制代碼。

    var db *sql.DB
    
    func main() {
        // Capture connection properties.
        cfg := mysql.NewConfig()
        cfg.User = os.Getenv("DBUSER")
        cfg.Passwd = os.Getenv("DBPASS")
        cfg.Net = "tcp"
        cfg.Addr = "127.0.0.1:3306"
        cfg.DBName = "recordings"
    
        // Get a database handle.
        var err error
        db, err = sql.Open("mysql", cfg.FormatDSN())
        if err != nil {
            log.Fatal(err)
        }
    
        pingErr := db.Ping()
        if pingErr != nil {
            log.Fatal(pingErr)
        }
        fmt.Println("Connected!")
    }
    

    在此程式碼中,您將執行以下操作:

    • 宣告一個型別為 *sql.DBdb 變數。這就是您的資料庫控制代碼。

      db 宣告為全域性變數簡化了本示例。在生產環境中,您應該避免使用全域性變數,例如透過將其作為引數傳遞給需要它的函式,或將其封裝在結構體中。

    • 使用 MySQL 驅動程式的 Config——以及該型別的 FormatDSN——來收集連線屬性並將它們格式化為 DSN 作為連線字串。

      使用 Config 結構體可以使程式碼比使用連線字串更容易閱讀。

    • 呼叫 sql.Open 初始化 db 變數,傳入 FormatDSN 的返回值。

    • 檢查 sql.Open 返回的錯誤。如果資料庫連線細節格式不正確,可能會失敗。

      為了簡化程式碼,您呼叫 log.Fatal 來終止執行並將錯誤列印到控制檯。在生產程式碼中,您會希望以更優雅的方式處理錯誤。

    • 呼叫 DB.Ping 確認連線資料庫是否工作正常。在執行時,sql.Open 可能不會立即連線,具體取決於驅動程式。您在這裡使用 Ping 來確認 database/sql 包在需要時可以連線。

    • 檢查 Ping 返回的錯誤,以防連線失敗。

    • 如果 Ping 成功連線,則列印一條訊息。

  2. 在 main.go 檔案頂部附近,緊接在 package 宣告下方,匯入支援您剛編寫的程式碼所需的包。

    檔案頂部現在應該如下所示:

    package main
    
    import (
        "database/sql"
        "fmt"
        "log"
        "os"
    
        "github.com/go-sql-driver/mysql"
    )
    
  3. 儲存 main.go。

執行程式碼

  1. 開始跟蹤 MySQL 驅動程式模組作為依賴項。

    使用 go get 將 github.com/go-sql-driver/mysql 模組新增為您自己模組的依賴項。使用點號引數表示“獲取當前目錄中程式碼的依賴項”。

    $ go get .
    go: added filippo.io/edwards25519 v1.1.0
    go: added github.com/go-sql-driver/mysql v1.8.1
    

    由於您在上一步中將其新增到了 import 宣告中,Go 下載了這個依賴項。有關依賴項跟蹤的更多資訊,請參閱新增依賴項

  2. 在命令列提示符下,為 Go 程式設定 DBUSERDBPASS 環境變數。

    在 Linux 或 Mac 上

    $ export DBUSER=username
    $ export DBPASS=password
    

    在 Windows 上

    C:\Users\you\data-access> set DBUSER=username
    C:\Users\you\data-access> set DBPASS=password
    
  3. 在包含 main.go 的目錄中,從命令列執行程式碼,輸入 go run 並使用點號引數,表示“運行當前目錄中的包”。

    $ go run .
    Connected!
    

您可以連線成功了!接下來,您將查詢一些資料。

查詢多行資料

在本節中,您將使用 Go 執行旨在返回多行的 SQL 查詢。

對於可能返回多行的 SQL 語句,您使用 database/sql 包中的 Query 方法,然後遍歷其返回的行。(您將在後面的 查詢單行資料 一節中學習如何查詢單行。)

編寫程式碼

  1. 在 main.go 中,緊接著 func main 的上方,貼上以下 Album 結構體定義。您將使用它來儲存查詢返回的行資料。

    type Album struct {
        ID     int64
        Title  string
        Artist string
        Price  float32
    }
    
  2. func main 下方,貼上以下 albumsByArtist 函式來查詢資料庫。

    // albumsByArtist queries for albums that have the specified artist name.
    func albumsByArtist(name string) ([]Album, error) {
        // An albums slice to hold data from returned rows.
        var albums []Album
    
        rows, err := db.Query("SELECT * FROM album WHERE artist = ?", name)
        if err != nil {
            return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
        }
        defer rows.Close()
        // Loop through rows, using Scan to assign column data to struct fields.
        for rows.Next() {
            var alb Album
            if err := rows.Scan(&alb.ID, &alb.Title, &alb.Artist, &alb.Price); err != nil {
                return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
            }
            albums = append(albums, alb)
        }
        if err := rows.Err(); err != nil {
            return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
        }
        return albums, nil
    }
    

    在此程式碼中,您將執行以下操作:

    • 宣告一個您定義的 Album 型別的 albums 切片。它將儲存返回行中的資料。結構體欄位名和型別對應於資料庫列名和型別。

    • 使用 DB.Query 執行 SELECT 語句,查詢指定藝術家姓名的專輯。

      Query 的第一個引數是 SQL 語句。該引數之後,您可以傳遞零個或多個任何型別的引數。這些引數提供了一個地方來指定 SQL 語句中引數的值。透過將 SQL 語句與引數值分開(而不是使用 fmt.Sprintf 等方式拼接),您可以讓 database/sql 包將值與 SQL 文字分開發送,從而消除任何 SQL 注入風險。

    • 延遲關閉 rows,以便在函式退出時釋放其持有的任何資源。

    • 遍歷返回的行,使用 Rows.Scan 將每行的列值賦值給 Album 結構體欄位。

      Scan 接收一個指向 Go 值的指標列表,列值將被寫入到這些指標指向的位置。此處,您傳遞了指向使用 & 運算子建立的 alb 變數中欄位的指標。Scan 透過指標寫入以更新結構體欄位。

    • 在迴圈內部,檢查將列值掃描到結構體欄位時是否發生錯誤。

    • 在迴圈內部,將新的 alb 新增到 albums 切片中。

    • 迴圈結束後,使用 rows.Err 檢查整個查詢是否發生錯誤。請注意,如果查詢本身失敗,這裡檢查錯誤是唯一發現結果不完整的方法。

  3. 更新您的 main 函式以呼叫 albumsByArtist

    func main 的末尾,新增以下程式碼。

    albums, err := albumsByArtist("John Coltrane")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Albums found: %v\n", albums)
    

    在新的程式碼中,您現在將執行以下操作:

    • 呼叫您新增的 albumsByArtist 函式,將其返回值賦給一個新的 albums 變數。

    • 列印結果。

執行程式碼

在包含 main.go 的目錄中,從命令列執行程式碼。

$ go run .
Connected!
Albums found: [{1 Blue Train John Coltrane 56.99} {2 Giant Steps John Coltrane 63.99}]

接下來,您將查詢單行資料。

查詢單行資料

在本節中,您將使用 Go 查詢資料庫中的單行資料。

對於您知道最多隻會返回一行的 SQL 語句,您可以使用 QueryRow,它比使用 Query 迴圈更簡單。

編寫程式碼

  1. albumsByArtist 下方,貼上以下 albumByID 函式。

    // albumByID queries for the album with the specified ID.
    func albumByID(id int64) (Album, error) {
        // An album to hold data from the returned row.
        var alb Album
    
        row := db.QueryRow("SELECT * FROM album WHERE id = ?", id)
        if err := row.Scan(&alb.ID, &alb.Title, &alb.Artist, &alb.Price); err != nil {
            if err == sql.ErrNoRows {
                return alb, fmt.Errorf("albumsById %d: no such album", id)
            }
            return alb, fmt.Errorf("albumsById %d: %v", id, err)
        }
        return alb, nil
    }
    

    在此程式碼中,您將執行以下操作:

    • 使用 DB.QueryRow 執行 SELECT 語句,查詢指定 ID 的專輯。

      它返回一個 sql.Row。為了簡化呼叫程式碼(您的程式碼!),QueryRow 不會返回錯誤。相反,它會安排在稍後的 Rows.Scan 中返回任何查詢錯誤(例如 sql.ErrNoRows)。

    • 使用 Row.Scan 將列值複製到結構體欄位中。

    • 檢查 Scan 返回的錯誤。

      特殊的錯誤 sql.ErrNoRows 表示查詢沒有返回任何行。通常,這個錯誤值得替換為更具體的文字,例如這裡的“沒有這樣的專輯”。

  2. 更新 main 函式以呼叫 albumByID

    func main 的末尾,新增以下程式碼。

    // Hard-code ID 2 here to test the query.
    alb, err := albumByID(2)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Album found: %v\n", alb)
    

    在新的程式碼中,您現在將執行以下操作:

    • 呼叫您新增的 albumByID 函式。

    • 列印返回的專輯 ID。

執行程式碼

在包含 main.go 的目錄中,從命令列執行程式碼。

$ go run .
Connected!
Albums found: [{1 Blue Train John Coltrane 56.99} {2 Giant Steps John Coltrane 63.99}]
Album found: {2 Giant Steps John Coltrane 63.99}

接下來,您將向資料庫中新增一張專輯。

新增資料

在本節中,您將使用 Go 執行 SQL INSERT 語句,向資料庫中新增新行。

您已經瞭解瞭如何將 QueryQueryRow 與返回資料的 SQL 語句一起使用。要執行不返回資料的 SQL 語句,您可以使用 Exec

編寫程式碼

  1. albumByID 下方,貼上以下 addAlbum 函式,將一張新專輯插入到資料庫中,然後儲存 main.go。

    // addAlbum adds the specified album to the database,
    // returning the album ID of the new entry
    func addAlbum(alb Album) (int64, error) {
        result, err := db.Exec("INSERT INTO album (title, artist, price) VALUES (?, ?, ?)", alb.Title, alb.Artist, alb.Price)
        if err != nil {
            return 0, fmt.Errorf("addAlbum: %v", err)
        }
        id, err := result.LastInsertId()
        if err != nil {
            return 0, fmt.Errorf("addAlbum: %v", err)
        }
        return id, nil
    }
    

    在此程式碼中,您將執行以下操作:

    • 使用 DB.Exec 執行 INSERT 語句。

      Query 一樣,Exec 接受一個 SQL 語句,後面跟著 SQL 語句的引數值。

    • 檢查嘗試 INSERT 操作時是否發生錯誤。

    • 使用 Result.LastInsertId 檢索插入的資料庫行的 ID。

    • 檢查嘗試檢索 ID 時是否發生錯誤。

  2. 更新 main 以呼叫新的 addAlbum 函式。

    func main 的末尾,新增以下程式碼。

    albID, err := addAlbum(Album{
        Title:  "The Modern Sound of Betty Carter",
        Artist: "Betty Carter",
        Price:  49.99,
    })
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("ID of added album: %v\n", albID)
    

    在新的程式碼中,您現在將執行以下操作:

    • 呼叫 addAlbum 新增一張新專輯,將新增的專輯的 ID 賦給 albID 變數。

執行程式碼

在包含 main.go 的目錄中,從命令列執行程式碼。

$ go run .
Connected!
Albums found: [{1 Blue Train John Coltrane 56.99} {2 Giant Steps John Coltrane 63.99}]
Album found: {2 Giant Steps John Coltrane 63.99}
ID of added album: 5

結論

恭喜您!您剛剛使用 Go 對關係型資料庫執行了簡單的操作。

建議的下一主題

完成的程式碼

本節包含您透過本教程構建的應用程式的程式碼。

package main

import (
    "database/sql"
    "fmt"
    "log"
    "os"

    "github.com/go-sql-driver/mysql"
)

var db *sql.DB

type Album struct {
    ID     int64
    Title  string
    Artist string
    Price  float32
}

func main() {
    // Capture connection properties.
    cfg := mysql.NewConfig()
    cfg.User = os.Getenv("DBUSER")
    cfg.Passwd = os.Getenv("DBPASS")
    cfg.Net = "tcp"
    cfg.Addr = "127.0.0.1:3306"
    cfg.DBName = "recordings"

    // Get a database handle.
    var err error
    db, err = sql.Open("mysql", cfg.FormatDSN())
    if err != nil {
        log.Fatal(err)
    }

    pingErr := db.Ping()
    if pingErr != nil {
        log.Fatal(pingErr)
    }
    fmt.Println("Connected!")

    albums, err := albumsByArtist("John Coltrane")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Albums found: %v\n", albums)

    // Hard-code ID 2 here to test the query.
    alb, err := albumByID(2)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Album found: %v\n", alb)

    albID, err := addAlbum(Album{
        Title:  "The Modern Sound of Betty Carter",
        Artist: "Betty Carter",
        Price:  49.99,
    })
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("ID of added album: %v\n", albID)
}

// albumsByArtist queries for albums that have the specified artist name.
func albumsByArtist(name string) ([]Album, error) {
    // An albums slice to hold data from returned rows.
    var albums []Album

    rows, err := db.Query("SELECT * FROM album WHERE artist = ?", name)
    if err != nil {
        return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
    }
    defer rows.Close()
    // Loop through rows, using Scan to assign column data to struct fields.
    for rows.Next() {
        var alb Album
        if err := rows.Scan(&alb.ID, &alb.Title, &alb.Artist, &alb.Price); err != nil {
            return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
        }
        albums = append(albums, alb)
    }
    if err := rows.Err(); err != nil {
        return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
    }
    return albums, nil
}

// albumByID queries for the album with the specified ID.
func albumByID(id int64) (Album, error) {
    // An album to hold data from the returned row.
    var alb Album

    row := db.QueryRow("SELECT * FROM album WHERE id = ?", id)
    if err := row.Scan(&alb.ID, &alb.Title, &alb.Artist, &alb.Price); err != nil {
        if err == sql.ErrNoRows {
            return alb, fmt.Errorf("albumsById %d: no such album", id)
        }
        return alb, fmt.Errorf("albumsById %d: %v", id, err)
    }
    return alb, nil
}

// addAlbum adds the specified album to the database,
// returning the album ID of the new entry
func addAlbum(alb Album) (int64, error) {
    result, err := db.Exec("INSERT INTO album (title, artist, price) VALUES (?, ?, ?)", alb.Title, alb.Artist, alb.Price)
    if err != nil {
        return 0, fmt.Errorf("addAlbum: %v", err)
    }
    id, err := result.LastInsertId()
    if err != nil {
        return 0, fmt.Errorf("addAlbum: %v", err)
    }
    return id, nil
}