☰ Mục lục series
Tài liệu Go tiếng Việt
  1. Bắt đầu (Getting started)
    1. Tổng quan
    2. Tải về và cài đặt
    3. Bắt đầu với Go
    4. Quản lý các bản cài đặt Go
    5. Plugin trình soạn thảo và IDE
  2. Dạo quanh Go (A Tour of Go)
    1. Chào mừng
    2. Package, biến và hàm
    3. Điều khiển luồng
    4. Struct, slice và map
    5. Method và interface
    6. Generics
    7. Xử lý đồng thời
  3. Hướng dẫn (Tutorials)
    1. Danh sách hướng dẫn
    2. Tạo một Go module
    3. Gọi mã của bạn từ một module khác
    4. Trả về và xử lý lỗi
    5. Trả về lời chào ngẫu nhiên
    6. Trả về lời chào cho nhiều người
    7. Thêm một test
    8. Biên dịch và cài đặt ứng dụng
    9. Kết luận
    10. Bắt đầu với workspace nhiều module
    11. Phát triển RESTful API với Go và Gin
    12. Truy cập cơ sở dữ liệu quan hệ
    13. Bắt đầu với generics
    14. Bắt đầu với fuzzing
    15. Làm việc với JSON
    16. Tìm và sửa các dependency có lỗ hổng với govulncheck
    17. Tìm và khắc phục phụ thuộc có lỗ hổng với VS Code Go
    18. Viết ứng dụng web
  4. Viết code Go
    1. Cách viết mã Go
    2. Effective Go
    3. Doc Comment trong Go
  5. Module
    1. Phát triển và publish module
    2. Quản lý phụ thuộc
    3. Tổ chức một module Go
    4. Quản lý mã nguồn của module
    5. Cách đánh số phiên bản module
    6. Quy trình phát hành và đánh phiên bản module
    7. Publish một module
    8. Phát triển một bản cập nhật major version
    9. Tham chiếu tệp go.mod
    10. Ngừng hỗ trợ 'go get' cho việc cài đặt tệp thực thi
  6. Cơ sở dữ liệu (Database)
    1. Truy cập cơ sở dữ liệu quan hệ
    2. Mở một handle cơ sở dữ liệu
    3. Thực thi câu lệnh SQL không trả về dữ liệu
    4. Truy vấn dữ liệu
    5. Dùng prepared statement
    6. Thực thi transaction
    7. Huỷ các thao tác đang chạy
    8. Quản lý kết nối
    9. Tránh rủi ro SQL injection
  7. Chẩn đoán và hiệu năng
    1. Chẩn đoán
    2. Hướng dẫn về Go Garbage Collector
    3. Tối ưu hoá dựa trên profile
    4. Data Race Detector
    5. Gỡ lỗi mã Go bằng GDB
    6. Hỗ trợ coverage profiling cho integration test
  8. Bảo mật (Security)
    1. Tổng quan bảo mật
    2. Thực hành tốt về bảo mật cho lập trình viên Go
    3. Quản lý lỗ hổng trong Go
    4. Cơ sở dữ liệu lỗ hổng của Go
    5. Quét lỗ hổng trong IDE
    6. Chính sách CNA của Go
    7. Go Fuzzing
    8. Chi tiết kỹ thuật của Go Fuzzing
    9. Chính sách bảo mật của Go
    10. Threat Model của Go
    11. Các quyết định về bảo mật của Go
    12. Tuân thủ FIPS 140-3
  9. Công cụ và toolchain
    1. Tài liệu về các lệnh
    2. Về lệnh go
    3. Go Toolchains
    4. Telemetry của Go
    5. Cài đặt Go từ mã nguồn
    6. Cài đặt và sử dụng gccgo
  10. Tham khảo
    1. Câu hỏi thường gặp (FAQ)
    2. Go 1 và tương lai của các chương trình Go
    3. Hướng dẫn chuyển đổi sang encoding/json/v2
    4. Hướng dẫn đóng góp
    5. Đóng góp cho frontend gccgo
  11. Phụ lục
    1. Bảng thuật ngữ
Cơ sở dữ liệu (Database)

Truy vấn dữ liệu

Bản dịch từ go.dev/doc/database/querying

Khi thực thi một câu lệnh SQL có trả về dữ liệu, hãy dùng một trong các hàm Query mà package database/sql cung cấp. Mỗi hàm trong số đó trả về một Row hoặc Rows mà bạn có thể sao chép dữ liệu sang các biến bằng hàm Scan. Chẳng hạn, bạn sẽ dùng những hàm này để thực thi câu lệnh SELECT.

Khi thực thi một câu lệnh không trả về dữ liệu, bạn có thể dùng hàm Exec hoặc ExecContext thay thế. Tìm hiểu thêm tại Thực thi câu lệnh SQL không trả về dữ liệu.

Package database/sql cung cấp hai cách để thực thi một truy vấn lấy kết quả.

  • Truy vấn một dòng duy nhất – QueryRow trả về nhiều nhất một Row từ cơ sở dữ liệu. Tìm hiểu thêm tại Truy vấn một dòng duy nhất.
  • Truy vấn nhiều dòng – Query trả về mọi dòng khớp dưới dạng một struct Rows mà mã của bạn có thể lặp qua. Tìm hiểu thêm tại Truy vấn nhiều dòng.

Nếu mã của bạn sẽ thực thi lặp đi lặp lại cùng một câu lệnh SQL, hãy cân nhắc dùng prepared statement. Tìm hiểu thêm tại Dùng prepared statement.

Chú ý: Đừng dùng các hàm định dạng chuỗi như fmt.Sprintf để ghép một câu lệnh SQL! Bạn có thể tạo ra nguy cơ SQL injection. Tìm hiểu thêm tại Tránh nguy cơ SQL injection.

Truy vấn một dòng duy nhất

QueryRow lấy về nhiều nhất một dòng của cơ sở dữ liệu, chẳng hạn khi bạn muốn tra cứu dữ liệu theo một ID duy nhất. Nếu truy vấn trả về nhiều dòng, hàm Scan sẽ bỏ qua tất cả trừ dòng đầu tiên.

QueryRowContext hoạt động giống QueryRow nhưng có thêm đối số context.Context. Tìm hiểu thêm tại Huỷ các thao tác đang chạy.

Ví dụ sau dùng một truy vấn để tìm xem tồn kho có đủ để phục vụ một lần mua hàng hay không. Câu lệnh SQL trả về true nếu đủ, false nếu không. Row.Scan sao chép giá trị trả về kiểu boolean vào biến enough thông qua một con trỏ.

func canPurchase(id int, quantity int) (bool, error) {
	var enough bool
	// Query for a value based on a single row.
	if err := db.QueryRow("SELECT (quantity >= ?) from album where id = ?",
		quantity, id).Scan(&enough); err != nil {
		if err == sql.ErrNoRows {
			return false, fmt.Errorf("canPurchase %d: unknown album", id)
		}
		return false, fmt.Errorf("canPurchase %d: %v", id, err)
	}
	return enough, nil
}

Lưu ý: Placeholder cho tham số trong prepared statement khác nhau tuỳ theo DBMS và driver mà bạn đang dùng. Ví dụ, driver pq dành cho Postgres yêu cầu placeholder dạng $1 thay vì ?.

Xử lý lỗi

Bản thân QueryRow không trả về lỗi. Thay vào đó, Scan báo mọi lỗi phát sinh từ thao tác tra cứu và scan gộp lại. Nó trả về sql.ErrNoRows khi truy vấn không tìm thấy dòng nào.

Các hàm trả về một dòng duy nhất

Hàm Mô tả
DB.QueryRow
DB.QueryRowContext
Chạy riêng lẻ một truy vấn trả về một dòng.
Tx.QueryRow
Tx.QueryRowContext
Chạy một truy vấn trả về một dòng bên trong một transaction lớn hơn. Tìm hiểu thêm tại Thực thi transaction.
Stmt.QueryRow
Stmt.QueryRowContext
Chạy một truy vấn trả về một dòng bằng một câu lệnh đã được chuẩn bị sẵn. Tìm hiểu thêm tại Dùng prepared statement.
Conn.QueryRowContext Dùng với các kết nối được giữ riêng. Tìm hiểu thêm tại Quản lý kết nối.

Truy vấn nhiều dòng

Bạn có thể truy vấn nhiều dòng bằng Query hoặc QueryContext, chúng trả về một Rows đại diện cho kết quả truy vấn. Mã của bạn duyệt qua các dòng trả về bằng Rows.Next. Mỗi lần lặp gọi Scan để sao chép giá trị các cột vào biến.

QueryContext hoạt động giống Query nhưng có thêm đối số context.Context. Tìm hiểu thêm tại Huỷ các thao tác đang chạy.

Ví dụ sau thực thi một truy vấn để trả về các album của một nghệ sĩ được chỉ định. Các album được trả về trong một sql.Rows. Mã dùng Rows.Scan để sao chép giá trị các cột vào những biến được biểu diễn bằng con trỏ.

func albumsByArtist(artist string) ([]Album, error) {
	rows, err := db.Query("SELECT * FROM album WHERE artist = ?", artist)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	// An album slice to hold data from returned rows.
	var albums []Album

	// 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, &alb.Quantity); err != nil {
			return albums, err
		}
		albums = append(albums, alb)
	}
	if err = rows.Err(); err != nil {
		return albums, err
	}
	return albums, nil
}

Hãy để ý lời gọi rows.Close được defer. Nó giải phóng mọi tài nguyên mà rows đang giữ, bất kể hàm kết thúc theo cách nào. Lặp hết toàn bộ các dòng cũng đóng rows một cách ngầm định, nhưng tốt hơn là dùng defer để chắc chắn rằng rows luôn được đóng.

Lưu ý: Placeholder cho tham số trong prepared statement khác nhau tuỳ theo DBMS và driver mà bạn đang dùng. Ví dụ, driver pq dành cho Postgres yêu cầu placeholder dạng $1 thay vì ?.

Xử lý lỗi

Hãy nhớ kiểm tra lỗi từ sql.Rows sau khi lặp qua kết quả truy vấn. Nếu truy vấn thất bại, đây là cách mã của bạn biết được điều đó.

Các hàm trả về nhiều dòng

Hàm Mô tả
DB.Query
DB.QueryContext
Chạy riêng lẻ một truy vấn.
Tx.Query
Tx.QueryContext
Chạy một truy vấn bên trong một transaction lớn hơn. Tìm hiểu thêm tại Thực thi transaction.
Stmt.Query
Stmt.QueryContext
Chạy một truy vấn bằng một câu lệnh đã được chuẩn bị sẵn. Tìm hiểu thêm tại Dùng prepared statement.
Conn.QueryContext Dùng với các kết nối được giữ riêng. Tìm hiểu thêm tại Quản lý kết nối.

Xử lý giá trị cột có thể null

Package database/sql cung cấp một số kiểu dữ liệu đặc biệt mà bạn có thể dùng làm đối số cho hàm Scan khi giá trị của một cột có thể là null. Mỗi kiểu đều có một trường Valid cho biết giá trị có khác null hay không, và một trường chứa giá trị đó nếu có.

Mã trong ví dụ sau truy vấn tên của một khách hàng. Nếu giá trị tên là null, mã sẽ thay bằng một giá trị khác để dùng trong ứng dụng.

var s sql.NullString
err := db.QueryRow("SELECT name FROM customer WHERE id = ?", id).Scan(&s)
if err != nil {
	log.Fatal(err)
}

// Find customer name, using placeholder if not present.
name := "Valued Customer"
if s.Valid {
	name = s.String
}

Xem thêm về từng kiểu trong tài liệu tham chiếu của package sql:

Lấy dữ liệu từ các cột

Khi lặp qua các dòng mà một truy vấn trả về, bạn dùng Scan để sao chép giá trị các cột của một dòng vào các giá trị Go, như mô tả trong tài liệu tham chiếu Rows.Scan.

Có một tập chuyển đổi dữ liệu cơ bản được mọi driver hỗ trợ, chẳng hạn chuyển INT của SQL sang int của Go. Một số driver mở rộng tập chuyển đổi này; xem tài liệu của từng driver để biết chi tiết.

Như bạn có thể đoán, Scan sẽ chuyển từ kiểu của cột sang kiểu dữ liệu Go tương tự. Ví dụ, Scan sẽ chuyển từ CHAR, VARCHAR và TEXT của SQL sang string của Go. Tuy nhiên, Scan cũng sẽ thực hiện chuyển đổi sang một kiểu Go khác phù hợp với giá trị của cột. Ví dụ, nếu cột là VARCHAR nhưng luôn chứa một con số, bạn có thể chỉ định một kiểu số của Go, chẳng hạn int, để nhận giá trị, và Scan sẽ chuyển đổi giúp bạn bằng strconv.Atoi.

Để biết chi tiết hơn về các chuyển đổi mà hàm Scan thực hiện, xem tài liệu tham chiếu Rows.Scan.

Xử lý nhiều tập kết quả

Khi thao tác cơ sở dữ liệu của bạn có thể trả về nhiều tập kết quả, bạn có thể lấy chúng bằng Rows.NextResultSet. Điều này hữu ích chẳng hạn khi bạn gửi câu lệnh SQL truy vấn riêng nhiều bảng, mỗi bảng trả về một tập kết quả.

Rows.NextResultSet chuẩn bị tập kết quả tiếp theo để một lời gọi Rows.Next lấy được dòng đầu tiên của tập kế tiếp đó. Nó trả về một giá trị boolean cho biết liệu có tập kết quả tiếp theo hay không.

Mã trong ví dụ sau dùng DB.Query để thực thi hai câu lệnh SQL. Tập kết quả đầu tiên đến từ truy vấn thứ nhất trong thủ tục, lấy về toàn bộ các dòng trong bảng album. Tập kết quả kế tiếp đến từ truy vấn thứ hai, lấy về các dòng từ bảng song.

rows, err := db.Query("SELECT * from album; SELECT * from song;")
if err != nil {
	log.Fatal(err)
}
defer rows.Close()

// Loop through the first result set.
for rows.Next() {
	// Handle result set.
}

// Advance to next result set.
rows.NextResultSet()

// Loop through the second result set.
for rows.Next() {
	// Handle second set.
}

// Check for any error in either result set.
if err := rows.Err(); err != nil {
	log.Fatal(err)
}

Nội dung gốc: Go Documentation © The Go Authors, giấy phép CC BY 3.0. Bản dịch và ghi chú: Lê Văn Dũng.