教程:訪問關係型資料庫

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

如果您對 Go 及其工具鏈有基本的瞭解,將能更好地學習本教程。如果這是您第一次接觸 Go,請參閱教程:Go 入門以獲取快速介紹。

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

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

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

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

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

先決條件

  • 安裝 MySQL 關係型資料庫管理系統 (DBMS)。
  • 安裝 Go。 有關安裝說明,請參閱安裝 Go
  • 一個程式碼編輯工具。 任何文字編輯器都可以。
  • 命令終端。 Go 在 Linux 和 Mac 上的任何終端以及 Windows 上的 PowerShell 或 cmd 中都能很好地工作。

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

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

  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 程式碼中,您

    • 刪除(刪除)一個名為 album 的表。首先執行此命令使您以後更容易重新執行指令碼,如果您想重新開始使用該表。

    • 建立一個包含四列的 album 表:idtitleartistprice。每行的 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.DB 型別的 db 變數。這是您的資料庫控制代碼。

      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 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
    

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

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

    在 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 對關係型資料庫執行了簡單的操作。

建議的後續主題

  • 檢視資料訪問指南,其中包含更多關於此處僅提及的主題的資訊。

  • 如果您是 Go 的新手,您會在Effective Go如何編寫 Go 程式碼中找到有用的最佳實踐。

  • Go 教程是 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
}