Bài 3: Nhập liệu với nhóm hàm Scan trong package fmt
Ngoài việc xuất dữ liệu ra (Output), package fmt còn cung cấp "gia tộc" Scan để nhận dữ liệu đầu vào (Input).
Nhóm hàm này thường được sử dụng khi bạn viết các ứng dụng CLI (Command Line Interface), tạo kịch bản tương tác (interactive scripts) để người dùng gõ phím trả lời, hoặc đọc dữ liệu dạng bảng từ các file văn bản thuần.
Giống như cấu trúc ma trận của nhóm Print, nhóm Scan cũng chia làm 3 họ và 3 định dạng:
-
Nguồn dữ liệu: Bàn phím (
Scan), Chuỗi (Sscan), hoặc luồng I/O (Fscan). -
Cách đọc: Thuần túy (
Scan), Đọc từng dòng (Scanln), Đọc theo mẫu (Scanf).
1. Nguyên lý cốt lõi: Truyền Con trỏ (Pointers)
Đây là điểm mà các kỹ sư mới học Go rất hay sai lầm.
Khi dùng hàm Print, bạn truyền giá trị của biến (truyền bản sao). Nhưng khi dùng hàm Scan, bạn đang yêu cầu Go lấy dữ liệu từ bàn phím và nhét nó vào trong một biến đã khai báo trước. Do đó, bạn bắt buộc phải truyền địa chỉ bộ nhớ (con trỏ - pointer) của biến đó bằng dấu &.
Go
package main
import "fmt"
func main() {
var name string
var age int
fmt.Print("Nhập tên và tuổi của bạn (cách nhau bởi khoảng trắng): ")
// SAI: fmt.Scan(name, age) -> Báo lỗi hoặc không lưu được dữ liệu
// ĐÚNG: Truyền con trỏ bằng toán tử &
fmt.Scan(&name, &age)
fmt.Printf("Xin chào %s, %d tuổi!\n", name, age)
}
2. Sự khác biệt giữa Scan và Scanln
Cả hai hàm này đều đọc dữ liệu từ os.Stdin (bàn phím) và phân tách các giá trị dựa trên khoảng trắng (spaces). Nhưng cách chúng xử lý ký tự xuống dòng (phím Enter) lại khác nhau.
-
fmt.Scan: Đọc lướt qua mọi ký tự trắng (bao gồm cả dấu cách, phím Tab, và phím Enter). Nó chỉ dừng lại khi đã nạp đủ dữ liệu cho tất cả các biến bạn yêu cầu, bất kể người dùng nhấn Enter bao nhiêu lần. -
fmt.Scanln: (Scan Line) Giống hệtScan, nhưng nó coi phím Enter (dấu\n) là tín hiệu kết thúc ngay lập tức. Nếu bạn yêu cầu 2 biến nhưng người dùng gõ 1 chữ rồi nhấn Enter,Scanlnsẽ ngừng lấy dữ liệu và biến thứ 2 sẽ giữ giá trị mặc định.
Ví dụ với Scanln (Khuyên dùng cho ứng dụng tương tác):
Go
package main
import "fmt"
func main() {
var width, height int
fmt.Print("Nhập chiều dài và chiều rộng (ví dụ: 10 5): ")
// Trả về số lượng biến đã đọc thành công (n) và lỗi (err) nếu có
n, err := fmt.Scanln(&width, &height)
if err != nil {
fmt.Printf("Lỗi nhập liệu: Chỉ đọc được %d giá trị. Chi tiết: %v\n", n, err)
return
}
fmt.Printf("Diện tích: %d\n", width*height)
}
3. Đọc dữ liệu theo cấu trúc chuẩn với Scanf
Scanf hoạt động ngược lại với Printf. Bạn cung cấp một chuỗi mẫu (template) chứa các Verbs (%s, %d), và Go sẽ ép người dùng (hoặc file văn bản) phải nhập dữ liệu đúng khớp với định dạng đó.
Kỹ thuật này cực kỳ lợi hại khi bạn cần bóc tách dữ liệu từ file log hoặc các chuỗi có định dạng cố định (như ngày tháng năm).
Go
package main
import "fmt"
func main() {
var user, ip string
// Yêu cầu dữ liệu phải khớp với cú pháp: "Login: [user] at [ip]"
fmt.Print("Dán log đăng nhập vào đây: ")
_, err := fmt.Scanf("Login: %s at %s", &user, &ip)
if err != nil {
fmt.Println("Định dạng không khớp!")
} else {
fmt.Printf("Đã phát hiện User: %s - IP: %s\n", user, ip)
}
}
4. Bóc tách dữ liệu từ chuỗi với Sscanf
Gia tộc Sscan (String Scan) đọc dữ liệu từ một biến string thay vì từ bàn phím. Đây là cách nhanh nhất để parse một chuỗi văn bản nếu bạn không muốn dùng Regex (Biểu thức chính quy) cồng kềnh.
Go
package main
import "fmt"
func main() {
// Trích xuất dữ liệu từ một chuỗi có sẵn (ví dụ lấy từ API hoặc File)
rawData := "Giao dịch GD-8890 hoàn tất với số tiền 500000 VNĐ"
var transID string
var amount int
// Dùng Sscanf để lấy đúng những chỗ có Verb %s và %d
fmt.Sscanf(rawData, "Giao dịch %s hoàn tất với số tiền %d VNĐ", &transID, &amount)
fmt.Printf("Đã trích xuất - ID: %s | Tiền: %d\n", transID, amount)
}
5. Hạn chế của nhóm Scan
Mặc dù tiện lợi, bạn cần lưu ý nhóm Scan không thể đọc một chuỗi có chứa khoảng trắng vào một biến duy nhất. Ví dụ: Nếu người dùng nhập tên là "Trần Văn A", Scan sẽ chia nó làm 3 phần. Nếu bạn chỉ truyền 1 biến &name, nó sẽ chỉ lấy được chữ "Trần".
Để đọc toàn bộ một câu dài có chứa dấu cách (từ bàn phím) cho đến khi nhấn Enter, các kỹ sư Go sẽ không dùng fmt.Scan mà chuyển sang dùng package bufio:
Go
// Kỹ thuật đọc chuỗi có khoảng trắng chuẩn xác nhất trong Go:
reader := bufio.NewReader(os.Stdin)
fmt.Print("Nhập họ tên đầy đủ: ")
fullName, _ := reader.ReadString('\n')
All rights reserved