Bài viết này sẽ hướng dẫn bạn những kiến thức nền tảng về ngôn ngữ lập trình Go (Golang), từ cài đặt môi trường, cấu trúc cơ bản, đến các tính năng cốt lõi và cách làm việc với chúng.
Cài đặt Go
Để bắt đầu với Go, bạn cần cài đặt Go SDK trên hệ thống của mình:
- Tru cập trang tải xuống chính thức: https://golang.org/dl/
- Chọn gói cài đặt phù hợp với hệ điều hành của bạn (Windows, macOS, Linux).
-
Thực hiện cài đặt:
- Windows/macOS: Chạy file cài đặt đã tải về và làm theo hướng dẫn.
- Linux: Giải nén gói đã tải về vào thư mục mong muốn (ví dụ:
/usr/local).
-
Thiết lập biến môi trường (chỉ cần thiết cho Linux, Windows/macOS thường tự động):
Thêm các dòng sau vào file cấu hình shell của bạn (ví dụ:
~/.bashrchoặc~/.zshrc), sau đó chạysource ~/.bashrc:export GOROOT=/path/to/go # Ví dụ: /usr/local/go export PATH=$PATH:$GOROOT/bin export GOPATH=$HOME/go_projects # Thư mục làm việc của bạn cho các dự án Go export PATH=$PATH:$GOPATH/binGOROOTtrỏ đến thư mục cài đặt Go.GOPATHlà nơi Go sẽ tìm các mã nguồn, gói và các file thực thi của bạn.PATHbao gồm các thư mụcbincủaGOROOTvàGOPATHđể bạn có thể chạy các lệnh Go từ bất kỳ đâu.
Thiết lập Môi trường Phát triển
Để có trải nghiệm phát triển tốt nhất với Go, chúng ta sẽ sử dụng Visual Studio Code (VS Code).
-
Cài đặt VS Code:
- Truy cập https://code.visualstudio.com/
- Tải và cài đặt phiên bản phù hợp với hệ điều hành của bạn.
-
Cài đặt Go Extension cho VS Code:
- Mở VS Code.
- Vào mục Extensions (
Ctrl+Shift+XhoặcCmd+Shift+X). - Tìm kiếm "Go" và cài đặt extension do Go Team phát triển.
-
Tạo dự án đầu tiên:
- Tạo một thư mục mới cho dự án của bạn, ví dụ:
$GOPATH/src/my_first_app. - Mở thư mục này trong VS Code (
File > Open Folder). - Trong VS Code, chuột phải vào thư mục và chọn
New File, đặt tên làmain.go. - Khi bạn lưu file, VS Code có thể sẽ gợi ý cài đặt các công cụ Go cần thiết (như
gopls,dlv, v.v.). Hãy nhấp vàoInstall All.
- Tạo một thư mục mới cho dự án của bạn, ví dụ:
-
Cài đặt công cụ Debugger (Delve):
Delve là trình gỡ lỗi mạnh mẽ cho Go. VS Code sử dụng Delve để debug.
- Mở terminal trong VS Code (
Ctrl+`hoặcCmd+`). - Chạy lệnh sau để cài đặt Delve:
go install github.com/go-delve/delve/cmd/dlv@latest - Mở terminal trong VS Code (
Các Tính năng Đặc trưng của Ngôn ngữ Go
1. Thu gom rác tự động (Garbage Collection)
- Go tự động quản lý bộ nhớ, giải phóng lập trình viên khỏi gánh nặng quản lý thủ công như trong C++.
- Điều này giúp bạn tập trung vào logic nghiệp vụ và giảm thiểu lỗi liên quan đến bộ nhớ.
- Chỉ cần cấp phát bộ nhớ bằng
new()hoặcmake(), không cần giải phóng.
2. Hỗ trợ lập trình Concurrency tự nhiên
- Go được thiết kế với khả năng xử lý đồng thời (concurrency) ngay từ cấp độ ngôn ngữ.
- Goroutines: Là các "tiến trình nhẹ" (lightweight threads) của Go. Bạn có thể tạo hàng nghìn, thậm chí hàng triệu goroutine một cách hiệu quả.
- Go sử dụng mô hình CSP (Communicating Sequential Processes) để quản lý concurrency.
- Ví dụ tạo một goroutine:
package main import "fmt" func main() { go fmt.Println("Xin chào từ một Goroutine!") fmt.Println("Xin chào từ Main Goroutine!") // Để thấy output từ goroutine, bạn có thể cần một chút độ trễ hoặc cơ chế đồng bộ hóa // Ví dụ đơn giản này có thể kết thúc trước khi goroutine kia kịp chạy. }
3. Kênh (Channels)
- Channels là các "đường ống" an toàn để các goroutine giao tiếp với nhau, tương tự như các đường ống (pipes) trong Unix/Linux.
- Chúng hỗ trợ truyền dữ liệu thuộc bất kỳ kiểu nào.
- Ví dụ sử dụng channel:
package main import "fmt" func main() { dataChannel := make(chan int, 3) // Tạo một channel với bộ đệm 3 phần tử dataChannel <- 10 // Gửi giá trị 10 vào channel dataChannel <- 20 // Gửi giá trị 20 vào channel receivedVal := <-dataChannel // Nhận giá trị từ channel fmt.Println("Giá trị nhận được:", receivedVal) // Output: Giá trị nhận được: 10 }
4. Đa giá trị trả về
- Một hàm trong Go có thể trả về nhiều giá trị. Điều này rất hữu ích cho việc trả về kết quả và lỗi cùng lúc.
- Ví dụ hàm trả về đa giá trị:
package main import "fmt" func calculateStats(val1, val2 int) (int, float64) { sumResult := val1 + val2 averageResult := float64(val1+val2) / 2.0 return sumResult, averageResult } func main() { total, average := calculateStats(5, 7) fmt.Printf("Tổng: %d, Trung bình: %.2f\n", total, average) // Output: Tổng: 12, Trung bình: 6.00 }
Chương trình "Hello, World!"
Dưới đây là chương trình "Hello, World!" kinh điển trong Go. Hãy tạo file main.go trong thư mục dự án của bạn và dán nội dung sau:
package main // Khai báo gói 'main', đây là gói bắt buộc cho các chương trình thực thi
import (
"fmt" // Import gói 'fmt' để định dạng và in output
)
func main() { // Hàm 'main' là điểm khởi đầu của mọi chương trình Go
fmt.Println("Chào thế giới từ Go!") // In chuỗi "Chào thế giới từ Go!" ra console
}
Để chạy chương trình, mở terminal trong thư mục my_first_app và gõ go run main.go.
Khái niệm về Gói (Packages)
- Trong Go, mã nguồn được tổ chức thành các gói, tương tự như module trong Python. Các gói cùng chức năng thường được đặt trong cùng một thư mục.
- Gói
main: Là gói đặc biệt dùng để tạo ra các chương trình thực thi. Mỗi ứng dụng Go chỉ có duy nhất một góimainvà phải chứa hàmmain(). - Mục đích chính của gói là tăng khả năng tái sử dụng mã.
Ví dụ sử dụng gói tùy chỉnh
1. Trong thư mục my_first_app, tạo một thư mục mới tên là math_ops.
2. Trong thư mục math_ops, tạo file arithmetic.go với nội dung sau:
package math_ops // Khai báo gói 'math_ops'
// Add thực hiện phép cộng hai số nguyên và trả về kết quả
func Add(a, b int) int { // Tên hàm bắt đầu bằng chữ hoa để có thể xuất khẩu ra ngoài gói
return a + b
}
// Subtract thực hiện phép trừ hai số nguyên
func Subtract(a, b int) int {
return a - b
}
3. Sửa đổi file main.go của bạn để sử dụng gói math_ops:
package main
import (
"fmt"
"my_first_app/math_ops" // Import gói math_ops. Đường dẫn dựa trên GOPATH/src
)
func main() {
num1 := 10
num2 := 5
// Gọi hàm Add từ gói math_ops
sumResult := math_ops.Add(num1, num2)
fmt.Printf("Tổng của %d và %d là: %d\n", num1, num2, sumResult) // Output: Tổng của 10 và 5 là: 15
// Gọi hàm Subtract
diffResult := math_ops.Subtract(num1, num2)
fmt.Printf("Hiệu của %d và %d là: %d\n", num1, num2, diffResult) // Output: Hiệu của 10 và 5 là: 5
}
Để chạy chương trình này, bạn cần đảm bảo rằng thư mục my_first_app được khởi tạo module Go. Mở terminal tại thư mục my_first_app và chạy:
go mod init my_first_app
go run main.go
Kiểu dữ liệu cơ bản và Toán tử
Quy tắc đặt tên file, từ khóa và định danh
- Tất cả các file mã nguồn Go đều có phần mở rộng
.go. - Định danh (Identifiers): Phải bắt đầu bằng một chữ cái hoặc dấu gạch dưới (
_). Go phân biệt chữ hoa, chữ thường. Ví dụ:myVariablevàMyVariablelà khác nhau. _(dấu gạch dưới) là một định danh đặc biệt, dùng để bỏ qua một giá trị (ví dụ: bỏ qua giá trị trả về của một hàm).- Go có các từ khóa dành riêng không thể dùng làm định danh (ví dụ:
func,var,package,import,if,for, v.v.).
Cấu trúc cơ bản của một chương trình Go
package main // Khai báo gói
import (
"fmt" // Nhập các gói cần thiết
)
func main() { // Hàm main, điểm bắt đầu thực thi
fmt.Println("Chào các bạn!")
}
- Mọi file mã nguồn Go đều phải thuộc về một gói.
- Từ khóa
importđược dùng để đưa các gói khác vào chương trình. - Một chương trình thực thi Go phải thuộc gói
mainvà có một hàmmain()duy nhất. - Gọi hàm:
- Hàm trong cùng gói: Gọi trực tiếp (ví dụ:
fmt.Println()nếu bạn tự định nghĩa hàmPrintlntrong góifmt). - Hàm từ gói khác: Gọi thông qua tên gói và tên hàm (ví dụ:
fmt.Println(),math_ops.Add()).
- Hàm trong cùng gói: Gọi trực tiếp (ví dụ:
- Quy tắc kiểm soát truy cập gói:
- Định danh (hàm, biến, kiểu) bắt đầu bằng chữ hoa là có thể xuất khẩu (exported), tức là có thể truy cập từ bên ngoài gói.
- Định danh bắt đầu bằng chữ thường là riêng tư (unexported), chỉ có thể truy cập bên trong gói đó.
Khai báo Hàm
Cú pháp cơ bản của khai báo hàm:
func TenHam(danhSachThamSo) (danhSachGiaTriTraVe) {
// Thân hàm
return ...
}
- Ví dụ:
// Hàm không có tham số và không có giá trị trả về func sayHello() { // ... } // Hàm với hai tham số kiểu int, không có giá trị trả về func addNumbers(x int, y int) { // ... } // Hàm với hai tham số kiểu int và trả về hai giá trị kiểu int func performOperation(x int, y int) (int, int) { // ... return x + y, x * y }
Hằng số (Constants)
- Hằng số được khai báo với từ khóa
const, giá trị của chúng không thể thay đổi sau khi khởi tạo. - Chỉ có thể khai báo hằng số cho các kiểu dữ liệu
boolean,number(int,float,complex) vàstring. - Cú pháp:
const identifier [type] = value. Kiểu dữ liệu (type) có thể bỏ qua nếu trình biên dịch có thể suy luận. - Ví dụ:
const appName string = "MyGoApp" const pi = 3.14159 // Trình biên dịch suy luận kiểu là float64 const ( statusOK = 200 statusError = 500 maxAttempts = 5 )
Biến (Variables)
- Cú pháp khai báo biến:
var identifier type - Ví dụ:
var counter int var message string // Khai báo và khởi tạo var initialValue int = 100 var greeting string = "Hello Go" // Khai báo ngắn gọn (chỉ dùng trong hàm) shortVar := "Đây là một chuỗi" anotherNum := 42 // Khai báo nhiều biến cùng lúc var ( userCount int userName string isActive bool version float32 = 1.2 )
Kiểu giá trị và Kiểu tham chiếu
- Kiểu giá trị (Value Types): Biến lưu trữ trực tiếp giá trị. Bộ nhớ thường được cấp phát trên stack. Khi gán hoặc truyền vào hàm, giá trị được sao chép. Các kiểu bao gồm:
int,float,bool,string,arrayvàstruct(nếu không chứa con trỏ). - Kiểu tham chiếu (Reference Types): Biến lưu trữ một địa chỉ bộ nhớ, nơi giá trị thực sự được đặt. Bộ nhớ thường được cấp phát trên heap và được GC quản lý. Khi gán hoặc truyền vào hàm, địa chỉ được sao chép. Các kiểu bao gồm:
pointers,slices,maps,channelsvàfunctions.
Phạm vi (Scope) của Biến
- Biến khai báo trong hàm được gọi là biến cục bộ (local variable), phạm vi sống giới hạn trong hàm đó.
- Biến khai báo ngoài hàm được gọi là biến toàn cục (global variable). Phạm vi sống là toàn bộ gói. Nếu biến toàn cục bắt đầu bằng chữ hoa, nó có thể truy cập từ toàn bộ chương trình (từ các gói khác).
Kiểu dữ liệu và Toán tử (tiếp)
Kiểu Boolean (bool)
- Chỉ có thể chứa
truehoặcfalse. - Ví dụ:
var isValid bool // Mặc định là false var isActive bool = true isCompleted := false - Toán tử liên quan:
!(NOT),&&(AND),||(OR).
Kiểu số (Numeric Types)
- Go cung cấp nhiều kiểu số nguyên (
int,int8,int16,int32,int64,uint,uint8,uint16,uint32,uint64) và số thực dấu phẩy động (float32,float64). - Chuyển đổi kiểu: Phải thực hiện tường minh bằng cú pháp
TargetType(variable). - Ví dụ:
package main import "fmt" func main() { var valInt int = 10 var valInt32 int32 valInt32 = int32(valInt + 5) // Chuyển đổi int sang int32 fmt.Println("Giá trị int32:", valInt32) // Output: Giá trị int32: 15 }
Kiểu ký tự (Character) và Chuỗi (String)
- Ký tự: Go sử dụng kiểu
byte(tên khác củauint8) để biểu diễn một byte đơn hoặcrune(tên khác củaint32) để biểu diễn một ký tự Unicode. - Ví dụ:
var charByte byte = 'A' // Biểu diễn mã ASCII của 'A' var charRune rune = '你好' // Biểu diễn ký tự Unicode - Chuỗi: Kiểu
stringdùng để biểu diễn một chuỗi ký tự. Chuỗi trong Go là bất biến (immutable). - Ví dụ:
package main import "fmt" func main() { var myString = "Chào Go!" fmt.Println("Chuỗi của tôi:", myString) // Output: Chuỗi của tôi: Chào Go! } - Cách biểu diễn chuỗi:
- Dấu nháy kép (
" "): Dùng cho các chuỗi thông thường, hỗ trợ các ký tự escape (\n,\t, v.v.). - Dấu nháy ngược (
` `): Dùng cho chuỗi thô (raw string literals). Mọi ký tự bên trong sẽ được giữ nguyên, bao gồm các ký tự ngắt dòng và không xử lý ký tự escape. Thích hợp cho biểu thức chính quy hoặc HTML/JSON.
- Dấu nháy kép (
Sử dụng gói strings và strconv
Gói strings cung cấp nhiều hàm tiện ích để thao tác với chuỗi, còn strconv dùng để chuyển đổi giữa chuỗi và các kiểu dữ liệu khác.
package main
import (
"fmt"
"strconv"
"strings"
)
func main() {
// --- Gói strings ---
sampleURL := "example.com/path"
if !strings.HasPrefix(sampleURL, "http://") {
sampleURL = fmt.Sprintf("http://%s", sampleURL)
}
fmt.Println("URL hoàn chỉnh:", sampleURL) // Output: URL hoàn chỉnh: http://example.com/path
filePath := "/home/user/document.txt"
fmt.Println("Có kết thúc bằng .txt không?", strings.HasSuffix(filePath, ".txt")) // Output: Có kết thúc bằng .txt không? true
text := "Xin chào Go, Go thật tuyệt!"
fmt.Println("Vị trí đầu tiên của 'Go':", strings.Index(text, "Go")) // Output: Vị trí đầu tiên của 'Go': 9
fmt.Println("Vị trí cuối cùng của 'Go':", strings.LastIndex(text, "Go")) // Output: Vị trí cuối cùng của 'Go': 13
replacedText := strings.Replace(text, "Go", "Golang", 1) // Thay thế "Go" bằng "Golang" lần đầu tiên
fmt.Println("Sau khi thay thế:", replacedText) // Output: Sau khi thay thế: Xin chào Golang, Go thật tuyệt!
fmt.Println("Số lần xuất hiện của 'Go':", strings.Count(text, "Go")) // Output: Số lần xuất hiện của 'Go': 2
repeatedText := strings.Repeat("ABC", 3)
fmt.Println("Lặp lại chuỗi:", repeatedText) // Output: Lặp lại chuỗi: ABCABCABC
fmt.Println("Chữ hoa:", strings.ToUpper(text)) // Output: Chữ hoa: XIN CHÀO GO, GO THẬT TUYỆT!
fmt.Println("Chữ thường:", strings.ToLower(text)) // Output: Chữ thường: xin chào go, go thật tuyệt!
// Cắt khoảng trắng
trimmedSpace := strings.TrimSpace(" một chuỗi có khoảng trắng ")
fmt.Println("Cắt khoảng trắng:", trimmedSpace) // Output: Cắt khoảng trắng: một chuỗi có khoảng trắng
// Cắt ký tự cụ thể ở đầu và cuối
trimmedChars := strings.Trim("---hello---", "-")
fmt.Println("Cắt ký tự:", trimmedChars) // Output: Cắt ký tự: hello
// Tách chuỗi thành slice
words := strings.Fields("Apple Banana Cherry") // Tách bằng khoảng trắng
fmt.Println("Các từ:", words) // Output: Các từ: [Apple Banana Cherry]
parts := strings.Split("red,green,blue", ",")
fmt.Println("Các phần:", parts) // Output: Các phần: [red green blue]
// Nối slice thành chuỗi
joinedString := strings.Join([]string{"file1.txt", "file2.log"}, "-")
fmt.Println("Nối chuỗi:", joinedString) // Output: Nối chuỗi: file1.txt-file2.log
// --- Gói strconv ---
numStr := strconv.Itoa(12345) // Chuyển số nguyên sang chuỗi
fmt.Println("Số nguyên thành chuỗi:", numStr) // Output: Số nguyên thành chuỗi: 12345
strNum := "987"
parsedNum, err := strconv.Atoi(strNum) // Chuyển chuỗi sang số nguyên
if err != nil {
fmt.Println("Lỗi chuyển đổi:", err)
} else {
fmt.Println("Chuỗi thành số nguyên:", parsedNum) // Output: Chuỗi thành số nguyên: 987
}
}
Kiểu Thời gian và Ngày tháng
Gói time cung cấp các chức năng để làm việc với thời gian.
- Sử dụng kiểu
time.Timeđể biểu diễn một thời điểm cụ thể. time.Now()để lấy thời gian hiện tại.- Truy cập các thành phần thời gian như
Day(),Minute(),Month(),Year(). time.Durationdùng để biểu diễn một khoảng thời gian (ví dụ: micro giây, mili giây, giây).
package main
import (
"fmt"
"time"
)
func main() {
currentTime := time.Now()
fmt.Println("Thời gian hiện tại:", currentTime) // Output: Thời gian hiện tại: 2023-10-27 10:30:00.123456789 +0700 ICT m=+0.000000001
// Truy cập các thành phần
fmt.Printf("Năm: %d, Tháng: %s, Ngày: %d\n", currentTime.Year(), currentTime.Month(), currentTime.Day())
// Định dạng thời gian
// Chuỗi định dạng "2006-01-02 15:04:05" là một chuỗi ma thuật trong Go, đại diện cho một thời điểm cụ thể (Jan 2, 2006, 3:04:05 PM)
fmt.Println("Định dạng YYYY/MM/DD HH:MM:SS:", currentTime.Format("2006/01/02 15:04:05"))
fmt.Println("Định dạng DD-MM-YYYY:", currentTime.Format("02-01-2006"))
// Đo lường thời gian trôi qua
startTime := time.Now().UnixNano()
time.Sleep(150 * time.Millisecond) // Dừng chương trình 150 mili giây
endTime := time.Now().UnixNano()
elapsedMicroseconds := (endTime - startTime) / 1000
fmt.Printf("Thời gian thực thi: %d micro giây\n", elapsedMicroseconds)
}
Kiểu Con trỏ (Pointers)
- Trong Go, các biến kiểu thông thường lưu trữ trực tiếp giá trị (kiểu giá trị).
- Để lấy địa chỉ bộ nhớ của một biến, sử dụng toán tử
&(address-of). Ví dụ:&myVar. - Kiểu con trỏ là biến lưu trữ một địa chỉ bộ nhớ.
- Để truy cập giá trị mà con trỏ đang trỏ tới (giải tham chiếu), sử dụng toán tử
*(dereference). Ví dụ:*ptrVar.
package main
import "fmt"
func main() {
var initialNum int = 50
fmt.Println("Giá trị của initialNum:", initialNum) // Output: Giá trị của initialNum: 50
fmt.Println("Địa chỉ của initialNum:", &initialNum) // Output: Địa chỉ của initialNum: 0xc0000140a0 (ví dụ)
// Khai báo một con trỏ kiểu *int
var ptrToNum *int
// Gán địa chỉ của initialNum cho ptrToNum
ptrToNum = &initialNum
fmt.Println("Giá trị của con trỏ ptrToNum (địa chỉ):", ptrToNum) // Output: Giá trị của con trỏ ptrToNum (địa chỉ): 0xc0000140a0
// Truy cập giá trị mà con trỏ đang trỏ tới
fmt.Println("Giá trị mà ptrToNum trỏ tới:", *ptrToNum) // Output: Giá trị mà ptrToNum trỏ tới: 50
// Thay đổi giá trị thông qua con trỏ
*ptrToNum = 150
fmt.Println("Giá trị mới của initialNum:", initialNum) // Output: Giá trị mới của initialNum: 150
var anotherNum int = 99
ptrToNum = &anotherNum // Bây giờ ptrToNum trỏ đến anotherNum
*ptrToNum = 500 // Thay đổi giá trị của anotherNum
fmt.Println("Giá trị của initialNum (không đổi):", initialNum) // Output: Giá trị của initialNum (không đổi): 150
fmt.Println("Giá trị mới của anotherNum:", anotherNum) // Output: Giá trị mới của anotherNum: 500
}
Điều khiển Luồng (Flow Control)
1. Câu lệnh if / else
package main
import "fmt"
func main() {
isRaining := true
if isRaining {
fmt.Println("Trời đang mưa, hãy mang theo ô!")
} else {
fmt.Println("Trời trong xanh, không cần ô.")
}
score := 85
if score >= 90 {
fmt.Println("Điểm A")
} else if score >= 70 {
fmt.Println("Điểm B")
} else {
fmt.Println("Điểm C")
}
}
2. Câu lệnh switch case
Câu lệnh switch trong Go rất linh hoạt, không cần break tường minh và có thể có nhiều điều kiện trong một case. Có thể sử dụng fallthrough để chuyển sang case tiếp theo.
package main
import "fmt"
func main() {
dayOfWeek := 3
switch dayOfWeek {
case 1:
fmt.Println("Chủ Nhật")
case 2, 3, 4, 5, 6: // Có thể có nhiều giá trị trong một case
fmt.Println("Ngày trong tuần")
default:
fmt.Println("Không phải ngày hợp lệ")
}
// switch với fallthrough
grade := "B"
switch grade {
case "A":
fmt.Println("Xuất sắc!")
fallthrough // Chuyển sang case tiếp theo
case "B":
fmt.Println("Khá giỏi!")
// Không có fallthrough, nên sẽ thoát khỏi switch
case "C":
fmt.Println("Trung bình")
}
// switch không có biểu thức, sử dụng điều kiện boolean
hour := 14
switch {
case hour < 12:
fmt.Println("Chào buổi sáng!")
case hour >= 12 && hour < 18:
fmt.Println("Chào buổi chiều!")
default:
fmt.Println("Chào buổi tối!")
}
// switch với short statement (khai báo biến trong switch)
switch status := "active"; status {
case "active":
fmt.Println("Trạng thái hoạt động")
case "inactive":
fmt.Println("Trạng thái không hoạt động")
default:
fmt.Println("Trạng thái không xác định")
}
}
Bài tập 11: Trò chơi Đoán số
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
rand.Seed(time.Now().UnixNano()) // Khởi tạo seed cho trình tạo số ngẫu nhiên
secretNum := rand.Intn(100) // Tạo số ngẫu nhiên từ 0 đến 99
fmt.Println("Chào mừng đến với trò chơi Đoán Số!")
fmt.Println("Tôi đã nghĩ một số từ 0 đến 99. Bạn hãy đoán xem!")
for {
var guess int
fmt.Print("Nhập số bạn đoán: ")
_, err := fmt.Scanf("%d\n", &guess) // Đọc số nguyên từ người dùng
if err != nil {
fmt.Println("Đầu vào không hợp lệ. Vui lòng nhập một số.")
continue
}
switch {
case guess == secretNum:
fmt.Println("Chúc mừng! Bạn đã đoán đúng!")
return // Thoát khỏi trò chơi
case guess < secretNum:
fmt.Println("Số bạn đoán nhỏ hơn số bí mật.")
case guess > secretNum:
fmt.Println("Số bạn đoán lớn hơn số bí mật.")
}
}
}
Bài tập 12: In hình tam giác
package main
import "fmt"
func printTrianglePattern() {
var rows int
fmt.Print("Nhập số hàng cho tam giác: ")
_, err := fmt.Scanf("%d\n", &rows)
if err != nil {
fmt.Println("Đầu vào không hợp lệ.")
return
}
for i := 1; i <= rows; i++ { // Duyệt qua từng hàng
for j := 0; j < i; j++ { // Duyệt qua từng cột để in ký tự
fmt.Print("*")
}
fmt.Println() // Xuống dòng sau mỗi hàng
}
}
func main() {
printTrianglePattern()
}
3. Câu lệnh for
Trong Go, for là câu lệnh vòng lặp duy nhất nhưng rất linh hoạt, có thể dùng thay thế cho while và do-while.
package main
import "fmt"
func main() {
// 1. Vòng lặp for truyền thống (với khởi tạo, điều kiện, thay đổi)
for i := 0; i < 5; i++ {
fmt.Printf("Vòng lặp truyền thống: %d\n", i)
}
// 2. Vòng lặp for như while (chỉ có điều kiện)
count := 0
for count < 3 {
fmt.Printf("Vòng lặp như while: %d\n", count)
count++
}
// 3. Vòng lặp vô hạn (infinite loop)
// for {
// fmt.Println("Vòng lặp vô hạn (Ctrl+C để thoát)")
// time.Sleep(time.Second) // Để không làm tràn console
// }
// 4. Vòng lặp for-range (để duyệt qua slice, array, map, string, channel)
greeting := "Chào Go Thế giới!"
for index, char := range greeting {
fmt.Printf("Ký tự tại index %d: %c (Unicode: %d)\n", index, char, char)
}
// 5. break và continue
for i := 0; i < 10; i++ {
if i%2 != 0 { // Nếu i là số lẻ
continue // Bỏ qua lần lặp hiện tại, chuyển sang lần lặp tiếp theo
}
if i > 6 {
break // Thoát khỏi vòng lặp khi i lớn hơn 6
}
fmt.Printf("Số chẵn nhỏ hơn 6: %d\n", i) // Output: 0, 2, 4, 6
}
}
4. Câu lệnh goto và label
goto được dùng để nhảy đến một nhãn (label) trong chương trình. Nên hạn chế sử dụng goto vì nó có thể làm mã khó đọc và bảo trì.
package main
import "fmt"
func main() {
i := 0
StartLoop: // Định nghĩa một nhãn
fmt.Println("Giá trị của i:", i)
i++
if i < 5 {
goto StartLoop // Nhảy đến nhãn StartLoop
}
// Ví dụ với nested loops và nhãn
OuterLoop:
for x := 0; x < 3; x++ {
for y := 0; y < 3; y++ {
if x == 1 && y == 1 {
continue OuterLoop // Nhảy đến vòng lặp ngoài
}
fmt.Printf("x: %d, y: %d\n", x, y)
}
}
}
Hàm (Functions)
Đặc điểm của hàm trong Go:
- Không hỗ trợ overloading: Trong cùng một gói, không thể có hai hàm cùng tên.
- Hàm là "first-class citizen": Hàm có thể được gán cho biến, truyền làm tham số cho hàm khác, hoặc trả về từ một hàm khác.
- Hỗ trợ hàm ẩn danh (anonymous functions) và đa giá trị trả về.
Gán hàm cho biến
package main
import "fmt"
func add(a, b int) int {
return a + b
}
func main() {
// Gán hàm 'add' cho biến 'sumFunc'
sumFunc := add
fmt.Printf("Kiểu của sumFunc: %T\n", sumFunc) // Output: Kiểu của sumFunc: func(int, int) int
result := sumFunc(20, 30) // Gọi hàm thông qua biến
fmt.Println("Kết quả phép cộng:", result) // Output: Kết quả phép cộng: 50
if sumFunc == add { // Có thể so sánh hàm
fmt.Println("sumFunc và add trỏ đến cùng một hàm")
}
}
Hàm bậc cao (Higher-Order Functions) và kiểu hàm
package main
import "fmt"
// Định nghĩa một kiểu hàm 'BinaryOperator'
type BinaryOperator func(int, int) int
// Hàm 'executeOperation' nhận một hàm BinaryOperator làm tham số
func executeOperation(op BinaryOperator, val1, val2 int) int {
return op(val1, val2)
}
func multiply(a, b int) int {
return a * b
}
func main() {
// Gán hàm 'multiply' cho biến kiểu BinaryOperator
mulOp := multiply
// Truyền biến hàm vào 'executeOperation'
result := executeOperation(mulOp, 10, 5)
fmt.Println("Kết quả phép nhân:", result) // Output: Kết quả phép nhân: 50
// Sử dụng một hàm ẩn danh trực tiếp
divisionResult := executeOperation(func(a, b int) int {
if b == 0 {
return 0 // Tránh chia cho 0
}
return a / b
}, 10, 2)
fmt.Println("Kết quả phép chia:", divisionResult) // Output: Kết quả phép chia: 5
}
Cách truyền tham số vào hàm
- Go sử dụng phương pháp truyền theo giá trị (pass by value) cho tất cả các loại tham số. Điều này có nghĩa là một bản sao của giá trị được truyền vào hàm.
- Tuy nhiên, đối với các kiểu tham chiếu như
slice,map,channel,interfacevà bản thânpointer, giá trị được sao chép là địa chỉ bộ nhớ mà chúng trỏ tới. Do đó, hàm vẫn có thể sửa đổi dữ liệu mà các tham chiếu này trỏ tới. - Ví dụ:
package main import "fmt" // Hàm nhận một số nguyên (kiểu giá trị) func modifyInt(num int) { num = 999 // Thay đổi 'num' trong hàm không ảnh hưởng đến biến gốc fmt.Println("Trong modifyInt, num =", num) } // Hàm nhận một con trỏ số nguyên func modifyIntByPointer(ptr *int) { *ptr = 999 // Thay đổi giá trị mà con trỏ trỏ tới, ảnh hưởng đến biến gốc fmt.Println("Trong modifyIntByPointer, *ptr =", *ptr) } // Hàm nhận một slice (kiểu tham chiếu, header được sao chép) func modifySlice(s []int) { s[0] = 100 // Thay đổi phần tử của slice, ảnh hưởng đến slice gốc fmt.Println("Trong modifySlice, s =", s) } func main() { a := 10 fmt.Println("Trước modifyInt, a =", a) // Output: Trước modifyInt, a = 10 modifyInt(a) fmt.Println("Sau modifyInt, a =", a) // Output: Sau modifyInt, a = 10 (không đổi) b := 20 fmt.Println("Trước modifyIntByPointer, b =", b) // Output: Trước modifyIntByPointer, b = 20 modifyIntByPointer(&b) fmt.Println("Sau modifyIntByPointer, b =", b) // Output: Sau modifyIntByPointer, b = 999 (đã đổi) mySlice := []int{1, 2, 3} fmt.Println("Trước modifySlice, mySlice =", mySlice) // Output: Trước modifySlice, mySlice = [1 2 3] modifySlice(mySlice) fmt.Println("Sau modifySlice, mySlice =", mySlice) // Output: Sau modifySlice, mySlice = [100 2 3] (đã đổi) }
Giá trị trả về được đặt tên
Bạn có thể đặt tên cho các giá trị trả về của hàm. Khi đó, bạn có thể sử dụng trực tiếp các tên này trong hàm và chỉ cần gọi return (naked return).
func processNumbers(a, b int) (sum int, product int) {
sum = a + b
product = a * b
return // Tương đương với return sum, product
}
Bỏ qua giá trị trả về bằng _
Sử dụng _ để bỏ qua các giá trị trả về mà bạn không cần.
sum, _ := processNumbers(10, 5) // Chỉ lấy tổng, bỏ qua tích
fmt.Println("Tổng:", sum)
Tham số biến đổi (Variadic Parameters)
Hàm có thể nhận số lượng tham số tùy ý của cùng một kiểu bằng cách sử dụng ....
// Hàm nhận 0 hoặc nhiều số nguyên
func sumAll(numbers ...int) int {
total := 0
for _, num := range numbers {
total += num
}
return total
}
// Hàm nhận ít nhất một số nguyên và các số nguyên khác tùy ý
func sumWithBase(base int, others ...int) int {
total := base
for _, num := range others {
total += num
}
return total
}
Tham số biến đổi (ví dụ: numbers) sẽ được xử lý bên trong hàm như một slice.
Mục đích của defer
Câu lệnh defer được dùng để hoãn việc thực thi một hàm cho đến khi hàm bao quanh nó kết thúc. Điều này thường được dùng cho việc dọn dẹp tài nguyên.
- Hàm
deferđược thực thi ngay trước khi hàm chứa nó trả về. - Nếu có nhiều câu lệnh
defer, chúng sẽ được thực thi theo thứ tự LIFO (Last-In, First-Out). - Các tham số của hàm
deferđược đánh giá tại thời điểmdeferđược khai báo, không phải khi nó thực sự chạy.
package main
import "fmt"
func exampleDefer() {
i := 0
defer fmt.Println("Giá trị của i khi defer chạy:", i) // i được đánh giá là 0 tại đây
i++
fmt.Println("Giá trị của i sau tăng:", i) // Output: Giá trị của i sau tăng: 1
return
}
func multipleDefer() {
for i := 0; i < 3; i++ {
defer fmt.Printf("%d ", i) // Output: 2 1 0 (LIFO)
}
fmt.Println("Hoàn thành vòng lặp.")
}
func main() {
exampleDefer()
fmt.Println("---")
multipleDefer()
fmt.Println("\n---")
// Ví dụ thực tế:
// 1. Đóng file
// file, err := os.Open("some.txt")
// if err != nil { /* xử lý lỗi */ }
// defer file.Close() // Đảm bảo file sẽ được đóng
// 2. Giải phóng khóa (lock)
// var mu sync.Mutex
// mu.Lock()
// defer mu.Unlock() // Đảm bảo khóa sẽ được giải phóng
// 3. Đóng kết nối cơ sở dữ liệu
// dbConn := openDatabase()
// defer dbConn.Close()
}
Tính nguyên tử (Atomicity)
Tính nguyên tử đề cập đến đặc tính mà một hoặc nhiều thao tác được thực hiện bởi CPU sẽ không bị gián đoạn. Các thao tác này được xem như một đơn vị không thể chia cắt; chúng hoặc là thực hiện tất cả, hoặc không thực hiện gì cả, và không có trạng thái trung gian nào được nhìn thấy từ bên ngoài.
Trong Go, gói sync/atomic cung cấp các hàm cho các phép toán nguyên tử trên các biến số học. Các phép toán này đảm bảo an toàn khi truy cập đồng thời mà không cần khóa mutex. Điều quan trọng là các tham số của các hàm nguyên tử phải là kiểu con trỏ, cho phép Go sử dụng các chỉ thị CPU đặc biệt để đảm bảo tính nguyên tử.
atomic.Value để đảm bảo an toàn đọc/ghi cho mọi giá trị
Gói atomic cũng cung cấp atomic.Value để lưu trữ và tải bất kỳ giá trị nào một cách nguyên tử. Điều này hữu ích để cập nhật cấu hình hoặc các đối tượng lớn mà không cần khóa mutex cho mỗi lần đọc/ghi.
Định nghĩa Struct
struct là một kiểu dữ liệu do người dùng định nghĩa, cho phép nhóm các trường (field) với các kiểu dữ liệu khác nhau lại với nhau thành một đơn vị logic.
1. Khai báo Struct
type Identifier struct {
field1 type1
field2 type2
// ...
}
// Ví dụ:
type User struct {
Name string
Age int
Email string
IsAdmin bool
}
2. Truy cập trường trong Struct
Sử dụng toán tử dấu chấm (.) để truy cập các trường.
var newUser User
newUser.Name = "Alice"
newUser.Age = 30
newUser.Email = "alice@example.com"
newUser.IsAdmin = false
fmt.Printf("Tên: %s, Tuổi: %d\n", newUser.Name, newUser.Age)
3. Ba cách khởi tạo Struct
package main
import "fmt"
type Product struct {
ID int
Name string
Price float64
}
func main() {
// 1. Khai báo biến struct và gán giá trị sau đó
var p1 Product
p1.ID = 1
p1.Name = "Laptop"
p1.Price = 1200.00
fmt.Println("Product 1:", p1)
// 2. Sử dụng new() để tạo con trỏ đến struct (khởi tạo với zero-value)
p2 := new(Product) // p2 là *Product
p2.ID = 2
p2.Name = "Mouse"
p2.Price = 25.50
fmt.Println("Product 2:", *p2) // Truy cập giá trị thông qua con trỏ
// 3. Sử dụng địa chỉ của struct literal (&Struct{})
p3 := &Product{ // p3 là *Product
ID: 3,
Name: "Keyboard",
Price: 75.00,
}
fmt.Println("Product 3:", *p3)
// Cũng có thể khởi tạo trực tiếp struct literal (giá trị struct)
p4 := Product{
ID: 4,
Name: "Monitor",
Price: 300.00,
}
fmt.Println("Product 4:", p4)
}
Với các biến con trỏ (ví dụ p2, p3), bạn có thể truy cập trường trực tiếp bằng p2.ID hoặc (*p2).ID. Go tự động giải tham chiếu cho bạn.
4. Bố cục bộ nhớ của Struct
Các trường của một struct được lưu trữ liên tiếp trong bộ nhớ. Điều này giúp truy cập hiệu quả.
5. Định nghĩa Cấu trúc dữ liệu liên kết (ví dụ: Linked List)
type Node struct {
Value int
Next *Node // Con trỏ tới Node tiếp theo
}
// Ví dụ Doubly Linked List
type DoublyNode struct {
Value int
Next *DoublyNode
Prev *DoublyNode // Con trỏ tới Node trước đó
}
// Ví dụ Binary Tree
type TreeNode struct {
Value int
Left *TreeNode // Con trỏ tới nút con bên trái
Right *TreeNode // Con trỏ tới nút con bên phải
}
6. Struct và chuyển đổi kiểu
Một struct là một kiểu dữ liệu riêng biệt và không thể ép kiểu sang một struct khác, ngay cả khi chúng có cùng các trường. Tuy nhiên, bạn có thể khai báo một kiểu mới dựa trên một struct hiện có.
type OldFormat struct {
ID int
}
type NewFormat OldFormat // NewFormat là một kiểu mới, không thể gán trực tiếp cho OldFormat
var oldData OldFormat
var newData NewFormat
// oldData = newData // Lỗi: cannot use newData (type NewFormat) as type OldFormat in assignment
oldData = OldFormat(newData) // Cần chuyển đổi tường minh
7. Factory Pattern cho Struct
Go không có hàm tạo (constructor) theo nghĩa truyền thống. Bạn có thể sử dụng hàm factory để tạo và khởi tạo struct.
// package models
type employee struct { // `employee` bắt đầu bằng chữ thường, nên nó là riêng tư
name string
age int
}
// NewEmployee là một hàm factory để tạo và trả về một con trỏ tới employee
// Hàm factory bắt đầu bằng chữ hoa để có thể truy cập từ bên ngoài gói
func NewEmployee(name string, age int) *employee {
return &employee{ // Trả về địa chỉ của một struct employee được khởi tạo
name: name,
age: age,
}
}
// trong main package
// emp := models.NewEmployee("David", 35)
8. make và new
make: Dùng để khởi tạo các kiểu dữ liệu tham chiếu nhưmap,slice,channel. Nó trả về một giá trị đã được khởi tạo.new: Dùng để cấp phát bộ nhớ cho bất kỳ kiểu dữ liệu nào (kiểu giá trị hoặc kiểu tham chiếu) và trả về một con trỏ tới bộ nhớ đã được cấp phát, khởi tạo với zero-value của kiểu đó.
9. Struct Tags
Bạn có thể thêm các thẻ (tag) vào các trường của struct. Các thẻ này là các chuỗi metadata và thường được sử dụng bởi các thư viện reflection (ví dụ: để serialize/deserialize JSON).
type Student struct {
Name string `json:"student_name" db:"full_name"` // Thẻ JSON và DB
Age int `json:"age"`
Score int `json:"-"` // Dấu gạch nối để bỏ qua trường này khi serialize/deserialize
}
10. Trường ẩn danh (Anonymous Fields)
Các trường trong struct có thể không có tên, khi đó tên của trường sẽ là tên kiểu của nó.
type Car struct {
Manufacturer string
Model string
}
type Truck struct {
Car // Trường ẩn danh kiểu Car
LoadCapacity int // Trường thông thường
int // Một trường ẩn danh kiểu int
}
func main() {
myTruck := Truck{
Car: Car{Manufacturer: "Ford", Model: "F-150"},
LoadCapacity: 2000,
int: 100, // Gán giá trị cho trường ẩn danh int
}
fmt.Println(myTruck.Model) // Có thể truy cập trực tiếp các trường của Car
fmt.Println(myTruck.LoadCapacity)
fmt.Println(myTruck.int) // Truy cập trường int ẩn danh
fmt.Println(myTruck.Car.Manufacturer) // Cách truy cập tường minh
}
11. Xử lý xung đột trường ẩn danh
Nếu có nhiều trường ẩn danh hoặc trường thông thường có cùng tên, bạn cần chỉ định rõ đường dẫn để truy cập.
type Engine struct {
Power int
}
type Wheel struct {
Size int
}
type Vehicle struct {
Engine // Trường ẩn danh
Wheel // Trường ẩn danh
Speed int
Power int // Trường thông thường trùng tên với Engine.Power
}
func main() {
v := Vehicle{}
v.Engine.Power = 100 // Truy cập trường Power của Engine
v.Wheel.Size = 18
v.Speed = 200
v.Power = 250 // Truy cập trường Power của Vehicle
fmt.Println(v.Engine.Power) // Output: 100
fmt.Println(v.Power) // Output: 250
}
12. Phương thức (Methods)
Phương thức trong Go là các hàm được gắn với một kiểu cụ thể (receiver). Bất kỳ kiểu dữ liệu nào do người dùng định nghĩa (struct, int, string, v.v.) đều có thể có phương thức.
Cú pháp: func (receiver Type) MethodName(parameters) (returnValues) {}
type MyInt int
func (m MyInt) IsEven() bool { // Phương thức IsEven cho kiểu MyInt
return m%2 == 0
}
type Point struct {
X, Y int
}
func (p Point) GetCoordinates() (int, int) { // Phương thức GetCoordinates cho kiểu Point
return p.X, p.Y
}
func main() {
var num MyInt = 4
fmt.Println("4 có phải số chẵn không?", num.IsEven()) // Output: 4 có phải số chẵn không? true
origin := Point{X: 0, Y: 0}
x, y := origin.GetCoordinates()
fmt.Printf("Tọa độ điểm gốc: (%d, %d)\n", x, y) // Output: Tọa độ điểm gốc: (0, 0)
}
13. Khác biệt giữa Hàm và Phương thức
- Hàm: Được gọi trực tiếp (ví dụ:
FunctionName(args)). - Phương thức: Được gọi thông qua một biến của kiểu receiver (ví dụ:
variable.MethodName(args)).
14. Receiver kiểu con trỏ vs. Receiver kiểu giá trị
Tương tự như cách truyền tham số vào hàm:
- Receiver kiểu giá trị: Phương thức nhận một bản sao của giá trị receiver. Thay đổi bên trong phương thức không ảnh hưởng đến biến gốc.
- Receiver kiểu con trỏ: Phương thức nhận một bản sao của con trỏ receiver (tức là địa chỉ của biến gốc). Thay đổi thông qua con trỏ sẽ ảnh hưởng đến biến gốc. Nên dùng khi cần sửa đổi receiver hoặc khi receiver là một struct lớn để tránh chi phí sao chép.
15. Kiểm soát truy cập Phương thức
Cũng như các hàm và trường, tên phương thức bắt đầu bằng chữ hoa là có thể xuất khẩu, chữ thường là riêng tư.
16. Kế thừa (Inheritance)
Go không có cơ chế kế thừa lớp theo nghĩa OOP truyền thống. Thay vào đó, nó sử dụng nhúng cấu trúc (struct embedding). Khi một struct nhúng một struct ẩn danh khác, nó sẽ tự động "kế thừa" các phương thức và trường của struct được nhúng.
type Person struct {
Name string
}
func (p Person) Greet() {
fmt.Printf("Chào, tôi là %s.\n", p.Name)
}
type Employee struct {
Person // Nhúng struct Person ẩn danh
Role string
}
func main() {
e := Employee{
Person: Person{Name: "Bob"},
Role: "Developer",
}
e.Greet() // Employee có thể gọi phương thức Greet của Person
fmt.Println("Vai trò:", e.Role)
}
17. Đa kế thừa (Multiple Inheritance)
Bạn có thể nhúng nhiều struct ẩn danh để đạt được hiệu ứng "đa kế thừa".
18. Thực hiện phương thức String()
Nếu một kiểu dữ liệu thực hiện phương thức String() string, thì hàm fmt.Println() (và các hàm định dạng khác) sẽ tự động gọi phương thức này để biểu diễn giá trị của biến.
type CustomType struct {
ID int
Value string
}
func (ct CustomType) String() string {
return fmt.Sprintf("ID: %d, Giá trị: %s", ct.ID, ct.Value)
}
func main() {
data := CustomType{ID: 1, Value: "Thông tin mẫu"}
fmt.Println(data) // Output: ID: 1, Giá trị: Thông tin mẫu
}
Giao diện (Interfaces)
interface định nghĩa một tập hợp các phương thức mà một kiểu dữ liệu phải thực hiện. Go interfaces không chứa bất kỳ trường dữ liệu nào, chỉ có chữ ký phương thức.
1. Định nghĩa Interface
type Shape interface {
Area() float64 // Phương thức Area trả về float64
Perimeter() float64 // Phương thức Perimeter trả về float64
}
2. Interface là một kiểu
Một biến kiểu interface có thể giữ bất kỳ giá trị nào của kiểu dữ liệu thực hiện tất cả các phương thức của interface đó.
var s Shape // Biến 's' có kiểu interface Shape
// s có thể chứa một Circle hoặc Rectangle nếu chúng implement Shape
3. Thực hiện Interface (Implicit Implementation)
- Go không có từ khóa
implements. Một kiểu dữ liệu tự động thực hiện một interface nếu nó cung cấp tất cả các phương thức mà interface đó yêu cầu. - Một kiểu có thể thực hiện nhiều interface.
- Nếu một kiểu chỉ thực hiện một phần các phương thức của interface, nó không thực hiện interface đó.
type Rectangle struct {
Width, Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return 3.14159 * c.Radius * c.Radius
}
func (c Circle) Perimeter() float6 // Lỗi: float chứ không phải float64
func (c Circle) Perimeter() float64 { // Sửa lỗi
return 2 * 3.14159 * c.Radius
}
func main() {
var myShape Shape // Khai báo biến interface
myShape = Rectangle{Width: 5, Height: 3}
fmt.Printf("Hình chữ nhật - Diện tích: %.2f, Chu vi: %.2f\n", myShape.Area(), myShape.Perimeter())
myShape = Circle{Radius: 4}
fmt.Printf("Hình tròn - Diện tích: %.2f, Chu vi: %.2f\n", myShape.Area(), myShape.Perimeter())
}
4. Đa hình (Polymorphism)
Interface cho phép bạn xử lý các đối tượng thuộc các kiểu khác nhau một cách thống nhất, miễn là chúng thực hiện cùng một interface.
5. Lồng Interface (Nested Interfaces)
Một interface có thể nhúng các interface khác, tạo ra một interface mới chứa tất cả các phương thức của các interface được nhúng.
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err)
}
// ReadWriter kết hợp cả Reader và Writer
type ReadWriter interface {
Reader
Writer
}
6. Khẳng định kiểu (Type Assertion)
Để truy cập kiểu cơ bản của một giá trị được lưu trữ trong interface, bạn có thể sử dụng khẳng định kiểu. Có hai dạng:
value := interfaceVar.(ConcreteType): Sẽ panic nếu kiểu không khớp.value, ok := interfaceVar.(ConcreteType): An toàn hơn,oksẽ làtruenếu kiểu khớp, ngược lại làfalse.
package main
import "fmt"
func main() {
var i interface{} = "Hello Go" // interface{} là empty interface, có thể chứa mọi kiểu
s := i.(string) // Khẳng định kiểu an toàn
fmt.Println(s) // Output: Hello Go
s, ok := i.(string)
fmt.Println(s, ok) // Output: Hello Go true
f, ok := i.(float64)
fmt.Println(f, ok) // Output: 0 false (không panic)
// f2 := i.(float64) // Sẽ panic nếu kiểu không khớp
// fmt.Println(f2)
}
7. type switch cho khẳng định kiểu
Sử dụng switch với .(type) để kiểm tra nhiều kiểu khác nhau một cách hiệu quả.
package main
import "fmt"
func classify(items ...interface{}) {
for i, x := range items {
switch v := x.(type) { // Khai báo biến 'v' chứa giá trị đã được chuyển đổi
case bool:
fmt.Printf("Tham số #%d là boolean: %t\n", i, v)
case float64:
fmt.Printf("Tham số #%d là float64: %.2f\n", i, v)
case int, int64: // Có thể kiểm tra nhiều kiểu cùng lúc
fmt.Printf("Tham số #%d là integer: %d\n", i, v)
case nil:
fmt.Printf("Tham số #%d là nil\n", i)
case string:
fmt.Printf("Tham số #%d là chuỗi: \"%s\"\n", i, v)
default:
fmt.Printf("Tham số #%d có kiểu không xác định: %T\n", i, v)
}
}
}
func main() {
classify(10, "Go", true, 3.14, nil, []int{1, 2}, map[string]string{"a": "b"})
}
8. Empty Interface (interface{})
Interface rỗng (interface{}) không có bất kỳ phương thức nào. Vì mọi kiểu dữ liệu đều thực hiện 0 phương thức, nên mọi giá trị đều có thể được gán cho một biến kiểu interface{}.
var anything interface{}
anything = 100
fmt.Println(anything) // Output: 100
anything = "một chuỗi bất kỳ"
fmt.Println(anything) // Output: một chuỗi bất kỳ
9. Kiểm tra xem một biến có thực hiện một interface cụ thể không
type Stringer interface {
String() string
}
type MyStruct struct {
Name string
}
func (ms MyStruct) String() string {
return "MyStruct: " + ms.Name
}
func main() {
var myVal MyStruct = MyStruct{Name: "Test"}
// Kiểm tra và khẳng định kiểu
if sVal, ok := interface{}(myVal).(Stringer); ok {
fmt.Printf("myVal thực hiện Stringer: %s\n", sVal.String())
} else {
fmt.Println("myVal không thực hiện Stringer")
}
// Một ví dụ khác, nếu không có String() method
type AnotherStruct struct {}
var anotherVal AnotherStruct
if _, ok := interface{}(anotherVal).(Stringer); ok {
fmt.Println("anotherVal thực hiện Stringer")
} else {
fmt.Println("anotherVal không thực hiện Stringer")
}
}
Sự khác biệt giữa Kiểu con trỏ và Kiểu giá trị (Chi tiết)
Trong Go, mọi thứ đều được truyền theo giá trị. Tuy nhiên, điều quan trọng là hiểu giá trị nào đang được truyền:
- Kiểu giá trị (int, float, bool, string, array, struct nhỏ): Khi gán hoặc truyền vào hàm, một bản sao hoàn chỉnh của dữ liệu được tạo. Thay đổi bản sao không ảnh hưởng đến bản gốc.
- Kiểu tham chiếu (slice, map, channel, function, pointer): Giá trị thực sự của các kiểu này là một cấu trúc dữ liệu nhỏ (gọi là "header") chứa một con trỏ tới dữ liệu cơ bản trên heap. Khi gán hoặc truyền vào hàm, bản sao của *header* này được tạo. Con trỏ trong bản sao vẫn trỏ đến *cùng một dữ liệu cơ bản* với bản gốc. Do đó, thay đổi dữ liệu cơ bản thông qua bản sao sẽ ảnh hưởng đến bản gốc.
Thí nghiệm với kiểu cơ bản
package main
import "fmt"
func main() {
fmt.Println("#### Thí nghiệm kiểu cơ bản ####")
valA := "originalA"
valB := "originalB"
fmt.Printf("Trước gán: valA = %s (&valA = %p), valB = %s (&valB = %p)\n", valA, &valA, valB, &valB)
valA = valB // Gán giá trị của valB cho valA. Tạo một bản sao mới của chuỗi "originalB" và gán cho valA.
fmt.Printf("Sau gán: valA = %s (&valA = %p), valB = %s (&valB = %p)\n", valA, &valA, valB, &valB)
valA = "changedA" // Thay đổi valA không ảnh hưởng đến valB
fmt.Printf("Sau thay đổi valA: valA = %s (&valA = %p), valB = %s (&valB = %p)\n", valA, &valA, valB, &valB)
// Kết quả: valA và valB có giá trị độc lập và địa chỉ khác nhau.
}
Thí nghiệm với kiểu tham chiếu (Struct)
package main
import "fmt"
type UserProfile struct {
Name string
Age int
}
func main() {
fmt.Println("\n#### Thí nghiệm kiểu tham chiếu (Struct Pointer) ####")
user1 := &UserProfile{Name: "Alice", Age: 30} // user1 là *UserProfile
user2 := &UserProfile{Name: "Bob", Age: 25} // user2 là *UserProfile
fmt.Printf("Trước gán: user1 = %v (&user1 = %p), user2 = %v (&user2 = %p)\n", user1, &user1, user2, &user2)
fmt.Printf(" user1.Name (&user1.Name = %p), user2.Name (&user2.Name = %p)\n", &user1.Name, &user2.Name)
user1 = user2 // user1 bây giờ trỏ tới cùng địa chỉ mà user2 trỏ tới
fmt.Printf("Sau gán: user1 = %v (&user1 = %p), user2 = %v (&user2 = %p)\n", user1, &user1, user2, &user2)
fmt.Printf(" user1.Name (&user1.Name = %p), user2.Name (&user2.Name = %p)\n", &user1.Name, &user2.Name)
user1.Name = "Charlie" // Thay đổi qua user1 sẽ thay đổi dữ liệu mà user2 cũng đang trỏ tới
fmt.Printf("Sau thay đổi user1.Name: user1 = %v, user2 = %v\n", user1, user2)
// Kết quả: user1 và user2 trỏ đến cùng một đối tượng UserProfile. Thay đổi qua user1 ảnh hưởng đến user2.
}
Thí nghiệm với Slice (hiện tượng "lạ")
Một slice là một kiểu tham chiếu, nhưng bản thân nó là một header chứa con trỏ tới mảng bên dưới, độ dài và dung lượng. Khi bạn truyền một slice vào hàm, header này được sao chép theo giá trị. Bản sao của header vẫn trỏ tới cùng mảng bên dưới.
- Nếu bạn sửa đổi các phần tử của slice thông qua bản sao của header, dữ liệu gốc sẽ bị thay đổi.
- Nếu bạn gán
nilcho bản sao của header slice trong hàm, nó chỉ làm thay đổi bản sao đó, không ảnh hưởng đến slice gốc (vì bạn đang thay đổi header, không phải dữ liệu cơ bản).
package main
import "fmt"
func modifySliceHeader(data []string) {
fmt.Printf(" modifySliceHeader: Địa chỉ header slice (bản sao): %p\n", &data)
data = nil // Chỉ gán nil cho bản sao của header slice trong hàm
fmt.Println(" modifySliceHeader: Sau gán nil, data =", data)
}
func modifySliceElements(data []string) {
fmt.Printf(" modifySliceElements: Địa chỉ header slice (bản sao): %p\n", &data)
if len(data) > 1 {
data[1] = "lisi" // Thay đổi phần tử của mảng cơ sở
}
fmt.Println(" modifySliceElements: Sau thay đổi phần tử, data =", data)
}
func main() {
mySlice := []string{"zhang", "san"}
fmt.Printf("main: Địa chỉ header slice gốc: %p\n", &mySlice)
fmt.Println("main: Trước modifySliceHeader, mySlice =", mySlice) // Output: [zhang san]
modifySliceHeader(mySlice) // Truyền bản sao của header slice
fmt.Println("main: Sau modifySliceHeader, mySlice =", mySlice) // Output: [zhang san] (không đổi)
fmt.Println("--------------------------------")
fmt.Println("main: Trước modifySliceElements, mySlice =", mySlice) // Output: [zhang san]
modifySliceElements(mySlice) // Truyền bản sao của header slice
fmt.Println("main: Sau modifySliceElements, mySlice =", mySlice) // Output: [zhang lisi] (đã đổi)
}
Giải thích: Hàm modifySliceHeader nhận một bản sao của header slice mySlice. Khi data = nil, nó chỉ làm cho bản sao này trỏ đến nil, chứ không ảnh hưởng đến header slice gốc mySlice trong hàm main. Ngược lại, modifySliceElements thay đổi một phần tử của mảng cơ sở mà cả header gốc và bản sao đều trỏ tới, nên thay đổi được phản ánh.
Gói reflect
Gói reflect cho phép chương trình kiểm tra cấu trúc của các đối tượng (biến, kiểu) tại thời điểm chạy (runtime introspection). Điều này hữu ích cho việc xây dựng các thư viện serialization, ORM, v.v.
1. Các chức năng chính
reflect.TypeOf(i interface{}) Type: Lấy kiểu động của biến, trả về đối tượngreflect.Type.reflect.ValueOf(i interface{}) Value: Lấy giá trị động của biến, trả về đối tượngreflect.Value.Value.Kind() Kind: Trả về "loại" cơ bản của biến (ví dụ:Int,String,Struct,Ptr).Value.Interface() interface{}: Chuyển đổi giá trịreflect.Valuetrở lại kiểuinterface{}.
2. Các hằng số reflect.Kind()
reflect.Kind là một tập hợp các hằng số đại diện cho các loại kiểu dữ liệu cơ bản trong Go (ví dụ: Bool, Int, String, Slice, Struct, v.v.).
3. Ví dụ Reflection
package main
import (
"fmt"
"reflect"
)
func main() {
var myFloat float64 = 6.28
fmt.Println("Kiểu tĩnh:", reflect.TypeOf(myFloat)) // Output: Kiểu tĩnh: float64
v := reflect.ValueOf(myFloat)
fmt.Println("Giá trị reflection:", v) // Output: Giá trị reflection: 6.28
fmt.Println("Kiểu reflection:", v.Type()) // Output: Kiểu reflection: float64
fmt.Println("Loại reflection:", v.Kind()) // Output: Loại reflection: float64
fmt.Println("Giá trị float:", v.Float()) // Output: Giá trị float: 6.28
// Chuyển đổi ngược về interface{} và sau đó về kiểu cụ thể
if val, ok := v.Interface().(float64); ok {
fmt.Printf("Giá trị từ interface{}: %.2f\n", val) // Output: Giá trị từ interface{}: 6.28
}
}
4. Lấy giá trị từ reflect.Value
Sử dụng các phương thức như Float(), Int(), String(), Bool() để lấy giá trị thực tế của biến.
5. Thay đổi giá trị biến bằng Reflection
Để thay đổi giá trị của một biến bằng reflection, bạn phải đảm bảo rằng giá trị reflect.Value bạn đang làm việc là "có địa chỉ" (addressable) và "có thể thiết lập" (settable). Điều này thường có nghĩa là bạn phải truyền một con trỏ đến biến.
package main
import (
"fmt"
"reflect"
)
func main() {
var num float64 = 1.0
// Lỗi: v là bản sao của giá trị num, không có địa chỉ.
// fv := reflect.ValueOf(num)
// fv.SetFloat(3.14) // Sẽ gây panic
// Cách đúng: Truyền con trỏ đến num
ptrToNum := reflect.ValueOf(&num) // ptrToNum là reflect.Value của *float64
fmt.Println("Có thể thiết lập?", ptrToNum.CanSet()) // Output: Có thể thiết lập? false (không thể thiết lập ptrToNum)
// Lấy phần tử mà con trỏ trỏ tới (Elem)
elemOfNum := ptrToNum.Elem() // elemOfNum là reflect.Value của float64 mà con trỏ trỏ tới
fmt.Println("Có thể thiết lập?", elemOfNum.CanSet()) // Output: Có thể thiết lập? true
elemOfNum.SetFloat(3.14)
fmt.Printf("Giá trị mới của num: %.2f\n", num) // Output: Giá trị mới của num: 3.14
}
6. Thao tác với Struct bằng Reflection
reflect.Value.NumField(): Lấy số lượng trường trong struct.reflect.Value.Field(i int) Value: Lấy giá trị của trường tại chỉ mụci.reflect.Value.Method(n int) Value: Lấy giá trị của phương thức tại chỉ mụcn.Value.Call(args []Value) []Value: Gọi một phương thức đã lấy được.
package main
import (
"fmt"
"reflect"
)
type MyObject struct {
Name string
Version int
}
func (mo MyObject) SayHello() string {
return "Hello từ " + mo.Name
}
func main() {
obj := MyObject{"GoApp", 1}
objValue := reflect.ValueOf(&obj).Elem() // Lấy Value của struct, không phải con trỏ
fmt.Printf("Struct có %d trường\n", objValue.NumField())
// Duyệt qua các trường
for i := 0; i < objValue.NumField(); i++ {
field := objValue.Field(i)
fieldType := objValue.Type().Field(i) // Lấy reflect.StructField để có tên
fmt.Printf("Trường %d: %s (%v) = %v\n", i, fieldType.Name, field.Type(), field.Interface())
}
// Thay đổi giá trị trường (chỉ khi trường là exported và Value là settable)
if objValue.Field(0).CanSet() { // Name là trường exported
objValue.Field(0).SetString("GoApplication")
}
if objValue.Field(1).CanSet() { // Version là trường exported
objValue.Field(1).SetInt(2)
}
fmt.Println("Struct sau thay đổi:", obj) // Output: Struct sau thay đổi: {GoApplication 2}
// Gọi phương thức (phương thức phải là exported)
method := reflect.ValueOf(obj).MethodByName("SayHello") // obj phải là giá trị, không phải con trỏ để gọi method
if method.IsValid() {
results := method.Call(nil) // nil vì không có tham số
fmt.Println("Kết quả gọi SayHello:", results[0].Interface())
}
}
Đọc và Ghi Terminal
1. Các biến file handle tiêu chuẩn
os.Stdin: Đầu vào chuẩn (bàn phím).os.Stdout: Đầu ra chuẩn (màn hình).os.Stderr: Đầu ra lỗi chuẩn (màn hình).
2. Đọc và ghi không đệm
Sử dụng gói fmt cho các thao tác I/O đơn giản.
package main
import (
"fmt"
)
func main() {
var name string
var age int
var height float32
fmt.Print("Nhập tên, tuổi và chiều cao của bạn: ")
// Đọc các giá trị cách nhau bởi khoảng trắng hoặc xuống dòng
_, err := fmt.Scan(&name, &age, &height) // Trả về số lượng item được đọc và lỗi
if err != nil {
fmt.Println("Lỗi đọc đầu vào:", err)
return
}
fmt.Printf("Bạn là %s, %d tuổi, cao %.2f mét.\n", name, age, height)
// fmt.Scanln đọc cho đến khi gặp ký tự xuống dòng
// fmt.Scanf đọc theo định dạng cụ thể
// Đọc từ một chuỗi (giống terminal)
inputString := "123 45.67 GoLang"
var num int
var fval float64
var text string
_, err = fmt.Sscanf(inputString, "%d %f %s", &num, &fval, &text)
if err != nil {
fmt.Println("Lỗi Sscanf:", err)
return
}
fmt.Printf("Từ chuỗi: Số=%d, Float=%.2f, Chuỗi=%s\n", num, fval, text)
}
3. Đọc có bộ đệm (Buffered Input)
Gói bufio cung cấp các trình đọc/ghi có bộ đệm, hiệu quả hơn cho các thao tác I/O lớn.
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
reader := bufio.NewReader(os.Stdin)
fmt.Print("Vui lòng nhập một dòng văn bản: ")
input, err := reader.ReadString('\n') // Đọc cho đến khi gặp ký tự xuống dòng
if err != nil {
fmt.Println("Lỗi đọc đầu vào:", err)
return
}
fmt.Printf("Đầu vào của bạn là: %s", input) // input đã bao gồm ký tự xuống dòng
}
Đọc và Ghi file
1. Đọc file
Sử dụng os.Open() để mở file, defer file.Close() để đảm bảo file được đóng.
package main
import (
"bufio"
"fmt"
"io"
"os"
)
func main() {
filePath := "example.txt" // Đảm bảo file này tồn tại hoặc tạo nó
// Tạo file mẫu nếu chưa có
_ = os.WriteFile(filePath, []byte("Dòng một.\nDòng hai.\nDòng ba."), 0644)
file, err := os.Open(filePath)
if err != nil {
fmt.Println("Lỗi mở file:", err)
return
}
defer file.Close() // Đảm bảo file được đóng khi hàm kết thúc
reader := bufio.NewReader(file)
line, err := reader.ReadString('\n') // Đọc một dòng đầu tiên
if err != nil && err != io.EOF {
fmt.Println("Lỗi đọc dòng đầu tiên:", err)
return
}
fmt.Printf("Đọc dòng đầu tiên: %s", line)
// Đọc toàn bộ file từng dòng một
fmt.Println("\n--- Đọc toàn bộ file ---")
file.Seek(0, io.SeekStart) // Đưa con trỏ về đầu file để đọc lại
for {
line, err := reader.ReadString('\n')
if err == io.EOF {
break // Đã đến cuối file
}
if err != nil {
fmt.Println("Lỗi đọc file:", err)
return
}
fmt.Printf("Dòng: %s", line)
}
}
2. Đếm ký tự trong file
package main
import (
"bufio"
"fmt"
"io"
"os"
"unicode" // Sử dụng gói unicode để xử lý ký tự đa ngôn ngữ
)
type FileCharCount struct {
Letters int
Digits int
Spaces int
Others int
}
func main() {
filePath := "sample.txt"
// Tạo file mẫu
_ = os.WriteFile(filePath, []byte("Hello Go 123. Xin chào Việt Nam!\n"), 0644)
file, err := os.Open(filePath)
if err != nil {
fmt.Println("Lỗi mở file:", err)
return
}
defer file.Close()
var charCounts FileCharCount
reader := bufio.NewReader(file)
for {
r, _, err := reader.ReadRune() // Đọc từng rune (ký tự Unicode)
if err == io.EOF {
break
}
if err != nil {
fmt.Println("Lỗi đọc ký tự:", err)
return
}
switch {
case unicode.IsLetter(r):
charCounts.Letters++
case unicode.IsDigit(r):
charCounts.Digits++
case unicode.IsSpace(r):
charCounts.Spaces++
default:
charCounts.Others++
}
}
fmt.Printf("Thống kê ký tự trong file:\n")
fmt.Printf(" Chữ cái: %d\n", charCounts.Letters)
fmt.Printf(" Chữ số: %d\n", charCounts.Digits)
fmt.Printf(" Khoảng trắng: %d\n", charCounts.Spaces)
fmt.Printf(" Khác: %d\n", charCounts.Others)
}
3. Ghi file
Sử dụng os.OpenFile() để mở file với các cờ (flags) và quyền (permissions).
- Cờ mở file:
os.O_WRONLY(chỉ ghi),os.O_CREATE(tạo file nếu không tồn tại),os.O_RDONLY(chỉ đọc),os.O_RDWR(đọc và ghi),os.O_TRUNC(cắt ngắn file về 0). - Quyền file: Dùng hệ bát phân (ví dụ:
0666cho đọc/ghi bởi mọi người,0644cho chủ sở hữu đọc/ghi, người khác chỉ đọc).
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
outputFilePath := "output.txt"
// Mở file để ghi (O_WRONLY), tạo file nếu không tồn tại (O_CREATE), và cắt ngắn file nếu đã có (O_TRUNC)
file, err := os.OpenFile(outputFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
fmt.Printf("Lỗi khi tạo/mở file: %v\n", err)
return
}
defer file.Close()
writer := bufio.NewWriter(file) // Sử dụng trình ghi có bộ đệm
contentToWrite := "Đây là dòng văn bản sẽ được ghi vào file.\n"
for i := 0; i < 5; i++ {
_, err := writer.WriteString(fmt.Sprintf("Dòng %d: %s", i+1, contentToWrite))
if err != nil {
fmt.Printf("Lỗi ghi file: %v\n", err)
return
}
}
err = writer.Flush() // Đảm bảo tất cả dữ liệu trong bộ đệm được ghi ra file
if err != nil {
fmt.Printf("Lỗi flush bộ đệm: %v\n", err)
return
}
fmt.Println("Ghi file thành công vào:", outputFilePath)
}
4. Sao chép file
Gói io cung cấp hàm Copy tiện lợi để sao chép nội dung từ một Reader sang một Writer.
package main
import (
"fmt"
"io"
"os"
)
// CopyFile sao chép nội dung từ srcName sang dstName
func CopyFile(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != nil {
return
}
defer src.Close()
dst, err := os.OpenFile(dstName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return
}
defer dst.Close()
return io.Copy(dst, src) // Sao chép dữ liệu từ src sang dst
}
func main() {
// Tạo file nguồn
_ = os.WriteFile("source.txt", []byte("Nội dung từ file nguồn."), 0644)
bytesCopied, err := CopyFile("destination.txt", "source.txt")
if err != nil {
fmt.Printf("Lỗi sao chép file: %v\n", err)
return
}
fmt.Printf("Sao chép thành công %d bytes từ source.txt sang destination.txt\n", bytesCopied)
}
Tham số dòng lệnh
os.Args: Một slice kiểustringchứa tất cả các đối số dòng lệnh, vớios.Args[0]là tên chương trình.- Gói
flag: Cung cấp cơ chế phân tích cú pháp các cờ (flags) dòng lệnh một cách có cấu trúc.package main import ( "flag" "fmt" ) func main() { // Khai báo các cờ var verbose bool var name string var count int flag.BoolVar(&verbose, "v", false, "Hiển thị thông tin chi tiết") flag.StringVar(&name, "name", "Người dùng", "Tên của người dùng") flag.IntVar(&count, "c", 1, "Số lần lặp") flag.Parse() // Phân tích cú pháp các cờ từ os.Args fmt.Printf("Chế độ chi tiết: %t\n", verbose) fmt.Printf("Tên: %s\n", name) fmt.Printf("Số lần lặp: %d\n", count) // Các đối số không phải cờ sẽ có trong flag.Args() fmt.Println("Các đối số còn lại:", flag.Args()) // Ví dụ chạy: go run your_program.go -v -name="Alice" -c 3 arg1 arg2 }
Đồng bộ hóa (Synchronization)
Khi nhiều goroutine cùng truy cập và sửa đổi một tài nguyên chung, cần có cơ chế đồng bộ hóa để tránh tình trạng race condition và đảm bảo tính đúng đắn của dữ liệu.
1. Các cơ chế đồng bộ hóa
- Mutex (Khóa tương hỗ): Một cơ chế khóa cơ bản. Chỉ một goroutine tại một thời điểm có thể giữ khóa mutex. Nếu một goroutine cố gắng lấy khóa đang được giữ, nó sẽ bị chặn cho đến khi khóa được giải phóng.
- Read-Write Mutex (Khóa đọc-ghi): Cho phép nhiều goroutine đọc tài nguyên cùng lúc (đọc chia sẻ), nhưng chỉ một goroutine được phép ghi tại một thời điểm (ghi độc quyền). Ghi ưu tiên hơn đọc.
- Goroutine: Là các "luồng" nhẹ của Go, chúng tự nhiên có khả năng xử lý đồng thời.
runtime.Goexit(): Kết thúc goroutine hiện tại. Các hàmdeferđã đăng ký sẽ được thực thi.runtime.Gosched(): Tạm dừng goroutine hiện tại và cho phép các goroutine khác chạy. Nó sẽ được lên lịch lại để chạy sau.runtime.GOMAXPROCS(): Kiểm soát số lượng lõi CPU mà chương trình Go được phép sử dụng.
Channel (Chi tiết)
Channel là một đường ống nơi các goroutine gửi và nhận giá trị.
1. Định nghĩa Channel
make(chan Kiểu_dữ_liệu, dung_lượng)
Kiểu_dữ_liệu: Kiểu dữ liệu mà channel sẽ truyền (ví dụ:int,string,struct{}).dung_lượng(tùy chọn): Số lượng giá trị mà channel có thể lưu trữ trước khi người gửi bị chặn.
ch := make(chan int) // Channel không có bộ đệm (unbuffered channel)
bufferedCh := make(chan string, 5) // Channel có bộ đệm dung lượng 5
2. Gửi và nhận dữ liệu
- Gửi:
ch <- value - Nhận:
value := <-ch
3. Channel không bộ đệm (Unbuffered Channel)
- Có dung lượng bằng 0 (
len = 0). Không thể lưu trữ dữ liệu. - Hoạt động theo cơ chế đồng bộ: Người gửi sẽ bị chặn cho đến khi có người nhận sẵn sàng, và người nhận sẽ bị chặn cho đến khi có người gửi sẵn sàng.
- Thường được dùng để đồng bộ hóa các goroutine.
4. Channel có bộ đệm (Buffered Channel)
- Có dung lượng lớn hơn 0 (
len > 0). Có thể lưu trữ dữ liệu lên đến dung lượng của nó. - Hoạt động theo cơ chế bất đồng bộ:
- Người gửi sẽ bị chặn chỉ khi channel đầy.
- Người nhận sẽ bị chặn chỉ khi channel trống.
5. Đóng Channel
- Sử dụng
close(ch)để đóng một channel. Không thể gửi thêm dữ liệu vào một channel đã đóng. - Người nhận có thể kiểm tra xem channel đã đóng hay chưa khi nhận dữ liệu:
value, ok := <-ch. Nếuoklàfalse, channel đã đóng vàvaluelà zero-value của kiểu dữ liệu. - Đóng channel quá sớm (khi vẫn còn goroutine gửi) sẽ gây panic.
- Có thể đọc từ một channel đã đóng. Nếu còn dữ liệu trong bộ đệm, nó sẽ đọc hết. Sau đó, nó sẽ đọc ra zero-value và
oksẽ làfalse.
6. Channel một chiều (Directional Channels)
Mặc định, channel là hai chiều. Bạn có thể chuyển đổi tường minh thành channel một chiều để tăng tính an toàn và rõ ràng của mã.
- Channel chỉ gửi:
var sendCh chan<- int - Channel chỉ nhận:
var recvCh <-chan int
Bạn có thể gán một channel hai chiều cho một channel một chiều. Điều ngược lại không thể thực hiện.
7. Ví dụ Producer-Consumer với Channel
package main
import (
"fmt"
"time"
)
type Order struct {
ID int
ProductID int
Quantity int
}
// Producer tạo ra các đơn hàng và gửi vào channel
func orderProducer(output chan<- Order, numOrders int) {
for i := 1; i <= numOrders; i++ {
newOrder := Order{
ID: i,
ProductID: 100 + i,
Quantity: i * 10,
}
output <- newOrder // Gửi đơn hàng vào channel
fmt.Printf("[Producer] Đã gửi đơn hàng #%d\n", newOrder.ID)
time.Sleep(50 * time.Millisecond) // Giả lập độ trễ
}
close(output) // Đóng channel khi tất cả đơn hàng đã được gửi
fmt.Println("[Producer] Hoàn thành việc tạo đơn hàng.")
}
// Consumer xử lý các đơn hàng nhận được từ channel
func orderConsumer(input <-chan Order, consumerID int) {
for order := range input { // Vòng lặp for-range sẽ tự động nhận dữ liệu và kết thúc khi channel đóng
fmt.Printf("[Consumer %d] Đã xử lý đơn hàng #%d (SP: %d, SL: %d)\n", consumerID, order.ID, order.ProductID, order.Quantity)
time.Sleep(100 * time.Millisecond) // Giả lập độ trễ xử lý
}
fmt.Printf("[Consumer %d] Hoàn thành việc xử lý đơn hàng.\n", consumerID)
}
func main() {
orderChannel := make(chan Order, 5) // Channel có bộ đệm dung lượng 5
// Khởi chạy Producer trong một goroutine riêng
go orderProducer(orderChannel, 10) // Tạo 10 đơn hàng
// Khởi chạy Consumer trong một goroutine riêng
go orderConsumer(orderChannel, 1)
// Để chương trình không kết thúc ngay lập tức, đợi một khoảng thời gian
time.Sleep(2 * time.Second)
fmt.Println("Chương trình chính kết thúc.")
}
定时器 (Timers) và Bộ đếm thời gian (Tickers)
Gói time cung cấp các cấu trúc Timer và Ticker để thực hiện các hành động theo thời gian.
1. Timer
Một Timer là một sự kiện duy nhất sẽ kích hoạt sau một khoảng thời gian định trước.
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println("Bắt đầu Timer...")
// Tạo một timer sẽ kích hoạt sau 2 giây
timer := time.NewTimer(2 * time.Second)
// <-timer.C sẽ chặn cho đến khi timer kích hoạt
<-timer.C
fmt.Println("Timer đã kích hoạt sau 2 giây!")
// Dừng Timer:
// Có thể dừng timer trước khi nó kích hoạt. Nếu timer đã kích hoạt, Stop sẽ trả về false.
stopped := timer.Stop()
if stopped {
fmt.Println("Timer đã được dừng.")
} else {
fmt.Println("Timer không thể dừng hoặc đã kích hoạt.")
}
// Đặt lại Timer:
// Có thể đặt lại timer để nó kích hoạt lại với một khoảng thời gian mới.
// Cần phải chắc chắn rằng không có giá trị nào đang chờ đọc trên channel C của timer
// trước khi gọi Reset, hoặc đọc nó nếu có.
timer.Reset(1 * time.Second)
fmt.Println("Timer đã được đặt lại 1 giây.")
<-timer.C
fmt.Println("Timer đã kích hoạt lần nữa sau khi reset!")
}
2. Ticker (Bộ đếm thời gian định kỳ)
Một Ticker gửi các sự kiện định kỳ trên channel của nó sau mỗi khoảng thời gian định trước.
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println("Bắt đầu Ticker...")
// Tạo một ticker sẽ gửi tín hiệu mỗi 500 mili giây
ticker := time.NewTicker(500 * time.Millisecond)
done := make(chan bool)
go func() {
for {
select {
case t := <-ticker.C:
fmt.Println("Ticker đã kích hoạt tại:", t.Format("15:04:05.000"))
case <-done:
fmt.Println("Goroutine ticker đã dừng.")
return
}
}
}()
// Để ticker chạy trong 3 giây
time.Sleep(3 * time.Second)
ticker.Stop() // Dừng ticker để giải phóng tài nguyên
done <- true // Báo hiệu goroutine ticker kết thúc
fmt.Println("Ticker đã dừng trong main.")
time.Sleep(100 * time.Millisecond) // Đợi goroutine kết thúc
}
Xử lý Timeout với select
select kết hợp với time.After() là một cách phổ biến để thêm thời gian chờ (timeout) vào các thao tác chặn.
package main
import (
"fmt"
"time"
)
func worker(done chan bool) {
fmt.Println("Worker đang làm việc...")
time.Sleep(3 * time.Second) // Giả lập công việc mất 3 giây
done <- true
}
func main() {
taskDone := make(chan bool, 1) // Sử dụng buffered channel để tránh deadlock nếu worker nhanh hơn timeout
go worker(taskDone)
select {
case <-taskDone:
fmt.Println("Công việc hoàn thành đúng hạn!")
case <-time.After(2 * time.Second): // Thời gian chờ 2 giây
fmt.Println("Timeout! Công việc không hoàn thành kịp.")
}
// Ví dụ khác: xử lý nhiều channel với timeout
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "Dữ liệu từ ch1"
}()
go func() {
time.Sleep(4 * time.Second)
ch2 <- "Dữ liệu từ ch2"
}()
for i := 0; i < 2; i++ { // Đợi 2 lần nhận dữ liệu hoặc timeout
select {
case msg := <-ch1:
fmt.Println("Nhận:", msg)
case msg := <-ch2:
fmt.Println("Nhận:", msg)
case <-time.After(2 * time.Second):
fmt.Println("Lặp", i+1, ": Đã hết thời gian chờ!")
}
}
}
Deadlock (Tắc nghẽn)
Deadlock là một tình huống trong đó hai hoặc nhiều tác vụ (goroutine) bị chặn vô thời hạn, mỗi tác vụ chờ đợi tài nguyên mà tác vụ khác đang giữ.
1. Các kịch bản Deadlock phổ biến
- Deadlock trong một goroutine: Một goroutine tự chặn chính nó. Ví dụ: gửi vào một unbuffered channel mà không có goroutine nào khác nhận.
// channel không bộ đệm sẽ gây deadlock nếu chỉ có một goroutine cố gắng gửi // func main() { // ch := make(chan int) // ch <- 1 // deadlock // } - Deadlock do thứ tự truy cập channel: Các goroutine chờ nhau theo một vòng tròn.
// func main() { // ch1 := make(chan int) // ch2 := make(chan int) // go func() { // <-ch1 // Goroutine này chờ ch1 // ch2 <- 1 // }() // <-ch2 // Main goroutine chờ ch2 // ch1 <- 1 // Main goroutine sẽ không bao giờ đến đây // } - Deadlock nhiều goroutine, nhiều channel/lock chéo nhau: Goroutine A giữ tài nguyên M và chờ N; Goroutine B giữ tài nguyên N và chờ M.
- Tránh trộn lẫn Mutex/RWMutex với Channel: Điều này có thể dẫn đến các deadlock khó phát hiện và hiểu. Nên ưu tiên sử dụng channel để đồng bộ hóa các goroutine trong Go.
Khóa tương hỗ (Mutex)
sync.Mutex cung cấp một khóa tương hỗ đơn giản để bảo vệ một phần mã hoặc dữ liệu khỏi việc truy cập đồng thời.
package main
import (
"fmt"
"sync"
"time"
)
var (
counter int
mu sync.Mutex // Khai báo một Mutex
)
func incrementCounter() {
mu.Lock() // Khóa mutex trước khi truy cập biến counter
defer mu.Unlock() // Đảm bảo mutex được mở khóa khi hàm kết thúc
counter++
fmt.Printf("Counter: %d\n", counter)
}
func main() {
var wg sync.WaitGroup // Để đợi tất cả goroutine hoàn thành
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
incrementCounter()
}()
}
wg.Wait() // Đợi tất cả goroutine hoàn thành
fmt.Printf("Giá trị cuối cùng của Counter: %d\n", counter) // Output: Giá trị cuối cùng của Counter: 5
}
Khóa đọc-ghi (RWMutex)
sync.RWMutex cung cấp khả năng khóa cho cả đọc và ghi. Nhiều goroutine có thể đọc đồng thời (RLock/RUnlock), nhưng chỉ một goroutine được phép ghi (Lock/Unlock). Khóa ghi sẽ chặn mọi khóa đọc/ghi khác.
package main
import (
"fmt"
"sync"
"time"
)
var (
sharedData int = 0
rwMu sync.RWMutex // Khai báo một RWMutex
)
func readData(id int) {
rwMu.RLock() // Khóa đọc
defer rwMu.RUnlock() // Mở khóa đọc
fmt.Printf("Đọc bởi Goroutine %d: %d\n", id, sharedData)
time.Sleep(100 * time.Millisecond) // Giả lập thời gian đọc
}
func writeData(id, value int) {
rwMu.Lock() // Khóa ghi
defer rwMu.Unlock() // Mở khóa ghi
sharedData = value
fmt.Printf("Ghi bởi Goroutine %d: Đã đặt giá trị thành %d\n", id, sharedData)
time.Sleep(200 * time.Millisecond) // Giả lập thời gian ghi
}
func main() {
var wg sync.WaitGroup
// Khởi tạo các Goroutine đọc
for i := 1; i <= 3; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
readData(id)
}(i)
}
// Khởi tạo một Goroutine ghi
wg.Add(1)
go func() {
defer wg.Done()
writeData(10, 50)
}()
// Khởi tạo thêm các Goroutine đọc sau khi ghi
for i := 4; i <= 6; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
time.Sleep(50 * time.Millisecond) // Đảm bảo ghi có thời gian chạy trước
readData(id)
}(i)
}
wg.Wait()
fmt.Println("Hoàn thành chương trình RWMutex.")
}
Biến điều kiện (Condition Variables)
sync.Cond được dùng để cho phép các goroutine đợi một điều kiện cụ thể trước khi tiếp tục thực thi. Nó luôn được liên kết với một sync.Locker (thường là sync.Mutex).
Quy trình sử dụng:
- Tạo một
sync.Cond, liên kết với mộtsync.Locker. - Khóa Mutex (
cond.L.Lock()). - Kiểm tra điều kiện: Nếu điều kiện chưa được đáp ứng, gọi
cond.Wait().Waitsẽ tự động mở khóa Mutex, chặn goroutine, và khi được đánh thức, nó sẽ khóa lại Mutex. - Sau khi điều kiện được đáp ứng, thực hiện các thao tác cần thiết.
- Mở khóa Mutex (
cond.L.Unlock()). - Thông báo cho các goroutine khác bằng
cond.Signal()(đánh thức một goroutine) hoặccond.Broadcast()(đánh thức tất cả các goroutine đang chờ).
package main
import (
"fmt"
"sync"
"time"
)
var (
queue = []int{} // Hàng đợi dữ liệu
maxSize = 3 // Dung lượng tối đa của hàng đợi
mu sync.Mutex
cond = sync.NewCond(&mu) // Khởi tạo biến điều kiện với mutex
)
// Producer
func producer(id int) {
for i := 0; i < 5; i++ {
mu.Lock() // Khóa trước khi truy cập hàng đợi
for len(queue) == maxSize {
fmt.Printf("Producer %d: Hàng đợi đầy, đang chờ...\n", id)
cond.Wait() // Hàng đợi đầy, chờ consumer
}
item := id*10 + i
queue = append(queue, item)
fmt.Printf("Producer %d: Đã sản xuất %d, Hàng đợi: %v\n", id, item, queue)
cond.Signal() // Báo hiệu cho consumer có dữ liệu mới
mu.Unlock() // Mở khóa
time.Sleep(100 * time.Millisecond)
}
}
// Consumer
func consumer(id int) {
for i := 0; i < 5; i++ {
mu.Lock() // Khóa trước khi truy cập hàng đợi
for len(queue) == 0 {
fmt.Printf("Consumer %d: Hàng đợi trống, đang chờ...\n", id)
cond.Wait() // Hàng đợi trống, chờ producer
}
item := queue[0]
queue = queue[1:]
fmt.Printf("Consumer %d: Đã tiêu thụ %d, Hàng đợi: %v\n", id, item, queue)
cond.Signal() // Báo hiệu cho producer có chỗ trống mới
mu.Unlock() // Mở khóa
time.Sleep(200 * time.Millisecond)
}
}
func main() {
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
producer(1)
}()
go func() {
defer wg.Done()
consumer(1)
}()
wg.Wait()
fmt.Println("Hoàn thành Producer-Consumer với Condition Variables.")
}
Kiến trúc Phân tầng Mạng
Mô hình TCP/IP là một kiến trúc mạng phân tầng được sử dụng rộng rãi.
Mô hình TCP/IP bốn tầng:
- Tầng ứng dụng (Application Layer): Tương tác trực tiếp với các ứng dụng người dùng. Các giao thức như HTTP, FTP, DNS.
- Tầng vận chuyển (Transport Layer): Cung cấp dịch vụ giao tiếp end-to-end giữa các tiến trình. Các giao thức chính:
- TCP (Transmission Control Protocol): Giao thức hướng kết nối, đáng tin cậy, truyền dữ liệu theo luồng.
- UDP (User Datagram Protocol): Giao thức không hướng kết nối, không đáng tin cậy, truyền dữ liệu theo gói tin (datagram).
- Port: Một số nguyên để nhận diện duy nhất một tiến trình trên một máy chủ (ví dụ: 80 cho HTTP, 443 cho HTTPS).
- Tầng mạng (Internet Layer): Chịu trách nhiệm định tuyến gói tin qua mạng. Giao thức chính:
- IP (Internet Protocol): Cung cấp địa chỉ logic (địa chỉ IP) để xác định duy nhất một thiết bị trên mạng.
- Tầng liên kết dữ liệu (Link Layer): Xử lý việc truyền dữ liệu qua một liên kết vật lý cục bộ. Các giao thức như Ethernet, Wi-Fi.
- MAC (Media Access Control) Address: Địa chỉ vật lý duy nhất của một card mạng.
- ARP (Address Resolution Protocol): Dùng để ánh xạ địa chỉ IP sang địa chỉ MAC.
Quá trình giao tiếp dữ liệu: Dữ liệu được đóng gói (encapsulation) với các header của từng tầng khi gửi và được giải đóng gói (decapsulation) khi nhận.
Socket: Một điểm cuối (endpoint) của giao tiếp hai chiều giữa hai chương trình trên mạng. Socket luôn xuất hiện theo cặp.
Máy chủ/Ứng dụng TCP Client-Server
Go cung cấp gói net để xây dựng các ứng dụng mạng.
Máy chủ TCP (TCP Server)
- Tạo một
Listenerđể lắng nghe kết nối đến:net.Listen("tcp", "IP:Port"). - Chấp nhận các kết nối đến bằng
listener.Accept(). MỗiAccepttrả về mộtConn(kết nối). - Đọc dữ liệu từ
Connbằngconn.Read(). - Xử lý dữ liệu.
- Ghi dữ liệu phản hồi vào
Connbằngconn.Write(). - Đóng
ListenervàConnkhi không còn cần thiết.
Sử dụng công cụ nc (netcat) để kiểm tra: nc 127.0.0.1 8000.
Ứng dụng TCP Client (TCP Client)
- Kết nối đến máy chủ:
net.Dial("tcp", "Server_IP:Server_Port"). Trả về mộtConn. - Ghi dữ liệu vào
Connbằngconn.Write(). - Đọc phản hồi từ
Connbằngconn.Read(). - Đóng
Conn.
Máy chủ TCP đa luồng (Concurrent TCP Server)
Để xử lý nhiều kết nối client đồng thời, máy chủ thường tạo một goroutine mới cho mỗi kết nối:
package main
import (
"fmt"
"io"
"net"
"strings"
"time"
)
// handleClientConnection xử lý một kết nối client riêng biệt
func handleClientConnection(conn net.Conn) {
defer conn.Close() // Đảm bảo đóng kết nối khi goroutine kết thúc
clientAddr := conn.RemoteAddr().String()
fmt.Printf("Client %s đã kết nối.\n", clientAddr)
buffer := make([]byte, 1024)
for {
// Đọc dữ liệu từ client
n, err := conn.Read(buffer)
if err != nil {
if err == io.EOF {
fmt.Printf("Client %s đã ngắt kết nối.\n", clientAddr)
} else {
fmt.Printf("Lỗi đọc từ client %s: %v\n", clientAddr, err)
}
return // Thoát khỏi vòng lặp khi có lỗi hoặc client ngắt kết nối
}
clientMessage := strings.TrimSpace(string(buffer[:n]))
fmt.Printf("Nhận từ %s: %s\n", clientAddr, clientMessage)
// Chuyển đổi tin nhắn thành chữ hoa và gửi lại
response := strings.ToUpper(clientMessage)
_, err = conn.Write([]byte("SERVER: " + response + "\n"))
if err != nil {
fmt.Printf("Lỗi ghi cho client %s: %v\n", clientAddr, err)
return
}
}
}
func main() {
listenAddr := "127.0.0.1:8000"
listener, err := net.Listen("tcp", listenAddr)
if err != nil {
fmt.Printf("Lỗi khi lắng nghe: %v\n", err)
return
}
defer listener.Close()
fmt.Printf("Máy chủ TCP đang lắng nghe trên %s\n", listenAddr)
for {
conn, err := listener.Accept() // Chờ và chấp nhận kết nối client mới
if err != nil {
fmt.Printf("Lỗi chấp nhận kết nối: %v\n", err)
continue // Tiếp tục lắng nghe các kết nối khác
}
go handleClientConnection(conn) // Xử lý mỗi kết nối trong một goroutine riêng
}
}
Ứng dụng TCP Client đa luồng (Concurrent TCP Client)
Một client có thể sử dụng goroutine để đọc từ bàn phím và gửi đến server, đồng thời đọc phản hồi từ server.
package main
import (
"bufio"
"fmt"
"net"
"os"
"strings"
)
func main() {
serverAddr := "127.0.0.1:8000" // Địa chỉ của máy chủ TCP
conn, err := net.Dial("tcp", serverAddr)
if err != nil {
fmt.Printf("Lỗi kết nối đến máy chủ: %v\n", err)
return
}
defer conn.Close()
fmt.Printf("Đã kết nối đến máy chủ %s\n", serverAddr)
// Goroutine để đọc từ server và in ra console
go func() {
reader := bufio.NewReader(conn)
for {
message, err := reader.ReadString('\n')
if err != nil {
fmt.Println("Lỗi đọc từ server hoặc server đã đóng:", err)
return
}
fmt.Print(message)
}
}()
// Đọc từ bàn phím và gửi đến server
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
text := scanner.Text()
_, err := fmt.Fprintf(conn, "%s\n", text) // Gửi văn bản + xuống dòng
if err != nil {
fmt.Println("Lỗi gửi dữ liệu:", err)
return
}
}
if err := scanner.Err(); err != nil {
fmt.Println("Lỗi đọc từ stdin:", err)
}
}
Quá trình giao tiếp TCP
- Ba bước bắt tay (Three-way Handshake): Thiết lập kết nối TCP đáng tin cậy.
- Client gửi gói tin SYN (Synchronize Sequence Number).
- Server nhận SYN, gửi lại SYN-ACK (Synchronize-Acknowledge).
- Client nhận SYN-ACK, gửi lại ACK (Acknowledge).
net.Dial()vàlistener.Accept()hoàn thành). - Bốn bước vẫy tay (Four-way Handshake): Kết thúc kết nối TCP.
- Bên chủ động đóng (Client/Server) gửi gói tin FIN (Finish).
- Bên bị động nhận FIN, gửi lại ACK. (Kết nối được đóng một nửa - half-close).
- Bên bị động gửi gói tin FIN của riêng nó.
- Bên chủ động gửi lại ACK.
TIME_WAIT(2MSL) đảm bảo gói ACK cuối cùng được nhận. - TCP là giao thức hướng kết nối và đáng tin cậy.
Giao tiếp UDP
UDP là giao thức không hướng kết nối, không đáng tin cậy, truyền dữ liệu theo gói tin (datagram).
Máy chủ UDP (UDP Server)
package main
import (
"fmt"
"net"
"time"
)
func main() {
serverAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:8001")
if err != nil {
fmt.Printf("Lỗi resolve địa chỉ UDP: %v\n", err)
return
}
conn, err := net.ListenUDP("udp", serverAddr) // Lắng nghe kết nối UDP
if err != nil {
fmt.Printf("Lỗi lắng nghe UDP: %v\n", err)
return
}
defer conn.Close()
fmt.Printf("Máy chủ UDP đang lắng nghe trên %s\n", serverAddr.String())
buffer := make([]byte, 1024)
for {
n, clientAddr, err := conn.ReadFromUDP(buffer) // Đọc dữ liệu từ client
if err != nil {
fmt.Printf("Lỗi đọc từ UDP: %v\n", err)
continue
}
message := string(buffer[:n])
fmt.Printf("Nhận từ %s: %s\n", clientAddr.String(), message)
// Gửi phản hồi
response := fmt.Sprintf("SERVER: Đã nhận '%s' lúc %s", message, time.Now().Format("15:04:05"))
_, err = conn.WriteToUDP([]byte(response), clientAddr)
if err != nil {
fmt.Printf("Lỗi ghi cho UDP client %s: %v\n", clientAddr.String(), err)
}
}
}
Ứng dụng UDP Client (UDP Client)
package main
import (
"bufio"
"fmt"
"net"
"os"
"time"
)
func main() {
serverAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:8001")
if err != nil {
fmt.Printf("Lỗi resolve địa chỉ UDP: %v\n", err)
return
}
conn, err := net.DialUDP("udp", nil, serverAddr) // Kết nối đến server UDP
if err != nil {
fmt.Printf("Lỗi dial UDP: %v\n", err)
return
}
defer conn.Close()
fmt.Printf("Đã kết nối đến server UDP %s\n", serverAddr.String())
// Goroutine để đọc phản hồi từ server
go func() {
buffer := make([]byte, 1024)
for {
n, _, err := conn.ReadFromUDP(buffer) // Đọc phản hồi
if err != nil {
fmt.Println("Lỗi đọc phản hồi từ server:", err)
return
}
fmt.Printf("Phản hồi từ server: %s\n", string(buffer[:n]))
}
}()
// Đọc từ bàn phím và gửi tin nhắn
reader := bufio.NewReader(os.Stdin)
for {
fmt.Print("Nhập tin nhắn để gửi: ")
text, _ := reader.ReadString('\n')
text = strings.TrimSpace(text)
if text == "exit" {
break
}
_, err := conn.Write([]byte(text)) // Gửi tin nhắn qua UDP
if err != nil {
fmt.Printf("Lỗi gửi tin nhắn: %v\n", err)
break
}
time.Sleep(500 * time.Millisecond) // Đợi phản hồi
}
fmt.Println("Client UDP đã thoát.")
}
Truyền file qua mạng
Truyền file thường sử dụng TCP vì tính đáng tin cậy của nó.
Chương trình gửi file (Client)
package main
import (
"fmt"
"io"
"net"
"os"
"path/filepath"
"strings"
)
func sendFile(conn net.Conn, filePath string) error {
file, err := os.Open(filePath)
if err != nil {
return fmt.Errorf("không thể mở file nguồn: %v", err)
}
defer file.Close()
// Gửi tên file trước
fileName := filepath.Base(filePath)
_, err = conn.Write([]byte(fileName + "\n"))
if err != nil {
return fmt.Errorf("không thể gửi tên file: %v", err)
}
// Đợi phản hồi "OK" từ server
responseBuffer := make([]byte, 1024)
n, err := conn.Read(responseBuffer)
if err != nil {
return fmt.Errorf("không thể đọc phản hồi từ server: %v", err)
}
if strings.TrimSpace(string(responseBuffer[:n])) != "OK" {
return fmt.Errorf("server từ chối nhận file: %s", string(responseBuffer[:n]))
}
// Gửi nội dung file
fmt.Printf("Đang gửi nội dung file '%s'...\n", fileName)
bytesSent, err := io.Copy(conn, file)
if err != nil {
return fmt.Errorf("lỗi khi gửi nội dung file: %v", err)
}
fmt.Printf("Đã gửi %d bytes.\n", bytesSent)
return nil
}
func main() {
if len(os.Args) < 2 {
fmt.Println("Cách dùng: go run client.go <đường_dẫn_file>")
return
}
filePath := os.Args[1]
// Tạo file mẫu nếu không tồn tại
if _, err := os.Stat(filePath); os.IsNotExist(err) {
_ = os.WriteFile(filePath, []byte("Đây là nội dung của file mẫu để truyền."), 0644)
fmt.Printf("Đã tạo file mẫu: %s\n", filePath)
}
serverAddr := "127.0.0.1:8002"
conn, err := net.Dial("tcp", serverAddr)
if err != nil {
fmt.Printf("Lỗi kết nối đến server: %v\n", err)
return
}
defer conn.Close()
fmt.Printf("Đã kết nối đến server %s để gửi file.\n", serverAddr)
err = sendFile(conn, filePath)
if err != nil {
fmt.Printf("Lỗi gửi file: %v\n", err)
} else {
fmt.Println("Gửi file hoàn tất.")
}
}
Chương trình nhận file (Server)
package main
import (
"bufio"
"fmt"
"io"
"net"
"os"
"strings"
"time"
)
func receiveFile(conn net.Conn) error {
reader := bufio.NewReader(conn)
// Đọc tên file
fileName, err := reader.ReadString('\n')
if err != nil {
return fmt.Errorf("không thể đọc tên file: %v", err)
}
fileName = strings.TrimSpace(fileName)
if fileName == "" {
return fmt.Errorf("tên file trống")
}
// Tạo file để ghi
outputFile, err := os.Create("received_" + fileName)
if err != nil {
return fmt.Errorf("không thể tạo file đích: %v", err)
}
defer outputFile.Close()
fmt.Printf("Đang nhận file '%s'...\n", fileName)
// Gửi phản hồi "OK" cho client
_, err = conn.Write([]byte("OK\n"))
if err != nil {
return fmt.Errorf("không thể gửi phản hồi OK cho client: %v", err)
}
// Nhận nội dung file
bytesReceived, err := io.Copy(outputFile, conn)
if err != nil {
return fmt.Errorf("lỗi khi nhận nội dung file: %v", err)
}
fmt.Printf("Đã nhận %d bytes vào file 'received_%s'.\n", bytesReceived, fileName)
return nil
}
func main() {
listenAddr := "127.0.0.1:8002"
listener, err := net.Listen("tcp", listenAddr)
if err != nil {
fmt.Printf("Lỗi lắng nghe: %v\n", err)
return
}
defer listener.Close()
fmt.Printf("Server nhận file đang lắng nghe trên %s\n", listenAddr)
for {
conn, err := listener.Accept()
if err != nil {
fmt.Printf("Lỗi chấp nhận kết nối: %v\n", err)
continue
}
go func(clientConn net.Conn) {
defer clientConn.Close()
clientAddr := clientConn.RemoteAddr().String()
fmt.Printf("Client %s đã kết nối để gửi file.\n", clientAddr)
err := receiveFile(clientConn)
if err != nil {
fmt.Printf("Lỗi xử lý file từ client %s: %v\n", clientAddr, err)
} else {
fmt.Printf("Hoàn thành xử lý file từ client %s.\n", clientAddr)
}
}(conn)
}
}
Phòng Chat (Chat Room)
Một ví dụ phức tạp hơn về ứng dụng mạng, sử dụng goroutine và channel để quản lý nhiều người dùng và tin nhắn.
Cấu trúc cơ bản:
- Một goroutine chính lắng nghe kết nối client.
- Mỗi client kết nối được xử lý bởi một goroutine riêng (
handleClientConnection). - Một goroutine quản lý trung tâm (
manager) xử lý các tin nhắn từ client và phát sóng đến tất cả người dùng trực tuyến. - Các channel được sử dụng để giao tiếp giữa các goroutine (ví dụ: gửi tin nhắn client đến manager, thông báo user online/offline).
package main
import (
"bufio"
"fmt"
"net"
"strings"
"sync"
"time"
)
// User đại diện cho một người dùng trong phòng chat
type User struct {
ID string
Name string
Conn net.Conn
MessageCh chan string // Kênh để gửi tin nhắn riêng cho người dùng này
LastActive time.Time
}
var (
users = make(map[string]*User) // Map lưu trữ người dùng trực tuyến (ID -> User)
usersMutex sync.RWMutex // Mutex để bảo vệ map users
joinCh = make(chan *User) // Kênh cho người dùng mới tham gia
leaveCh = make(chan *User) // Kênh cho người dùng rời đi
messageCh = make(chan string) // Kênh cho tất cả tin nhắn để phát sóng
)
// manager Goroutine quản lý trạng thái phòng chat và phát sóng tin nhắn
func manager() {
for {
select {
case newUser := <-joinCh:
usersMutex.Lock()
users[newUser.ID] = newUser
usersMutex.Unlock()
broadcastMessage(fmt.Sprintf("%s đã tham gia phòng chat.", newUser.Name))
sendUserList(newUser) // Gửi danh sách người dùng cho người dùng mới
case leavingUser := <-leaveCh:
usersMutex.Lock()
delete(users, leavingUser.ID)
usersMutex.Unlock()
broadcastMessage(fmt.Sprintf("%s đã rời phòng chat.", leavingUser.Name))
case msg := <-messageCh:
broadcastMessage(msg)
}
}
}
// broadcastMessage gửi tin nhắn đến tất cả người dùng trực tuyến
func broadcastMessage(msg string) {
usersMutex.RLock()
defer usersMutex.RUnlock()
for _, user := range users {
select {
case user.MessageCh <- msg:
default:
// Nếu kênh tin nhắn của người dùng đầy, bỏ qua tin nhắn
fmt.Printf("Cảnh báo: Kênh tin nhắn của %s đầy, không thể gửi: %s\n", user.Name, msg)
}
}
}
// sendUserList gửi danh sách người dùng cho một người dùng cụ thể
func sendUserList(targetUser *User) {
usersMutex.RLock()
defer usersMutex.RUnlock()
list := []string{}
for _, u := range users {
list = append(list, u.Name)
}
targetUser.MessageCh <- fmt.Sprintf("Người dùng trực tuyến (%d): %s", len(list), strings.Join(list, ", "))
}
// handleClientConnection xử lý giao tiếp với từng client
func handleClientConnection(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
conn.Write([]byte("Chào mừng đến với phòng chat! Vui lòng nhập tên của bạn: \n"))
name, err := reader.ReadString('\n')
if err != nil {
fmt.Println("Lỗi đọc tên:", err)
return
}
name = strings.TrimSpace(name)
if name == "" {
name = fmt.Sprintf("Người dùng ẩn danh_%d", time.Now().UnixNano()%1000)
}
userID := conn.RemoteAddr().String()
currentUser := &User{
ID: userID,
Name: name,
Conn: conn,
MessageCh: make(chan string, 100), // Kênh tin nhắn cho người dùng này
LastActive: time.Now(),
}
joinCh <- currentUser // Gửi thông báo người dùng tham gia
// Goroutine để gửi tin nhắn từ kênh của người dùng đến client
go func() {
for msg := range currentUser.MessageCh {
_, err := currentUser.Conn.Write([]byte(msg + "\n"))
if err != nil {
fmt.Printf("Lỗi gửi tin nhắn cho %s: %v\n", currentUser.Name, err)
return // Thoát khỏi goroutine gửi
}
}
}()
// Thời gian chờ cho hoạt động của người dùng (ví dụ: 5 phút)
inactivityTimeout := 5 * time.Minute
timeoutCh := time.After(inactivityTimeout)
// Đọc tin nhắn từ client
for {
select {
case <-timeoutCh:
currentUser.MessageCh <- fmt.Sprintf("Bạn đã bị ngắt kết nối do không hoạt động trong %s.", inactivityTimeout)
fmt.Printf("Người dùng %s đã bị ngắt kết nối do không hoạt động.\n", currentUser.Name)
leaveCh <- currentUser
return // Ngắt kết nối
default:
conn.SetReadDeadline(time.Now().Add(1 * time.Second)) // Đặt thời gian chờ đọc ngắn
message, err := reader.ReadString('\n')
if err != nil {
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
// Lỗi timeout khi đọc, tiếp tục kiểm tra các trường hợp khác
continue
}
fmt.Printf("Client %s (%s) đã ngắt kết nối: %v\n", currentUser.Name, currentUser.ID, err)
leaveCh <- currentUser // Gửi thông báo người dùng rời đi
return
}
message = strings.TrimSpace(message)
currentUser.LastActive = time.Now() // Cập nhật thời gian hoạt động
// Xử lý các lệnh chat
if strings.HasPrefix(message, "/name ") {
newName := strings.TrimPrefix(message, "/name ")
newName = strings.TrimSpace(newName)
if newName != "" {
oldName := currentUser.Name
currentUser.Name = newName
usersMutex.Lock()
users[currentUser.ID] = currentUser // Cập nhật trong map
usersMutex.Unlock()
broadcastMessage(fmt.Sprintf("%s đã đổi tên thành %s.", oldName, newName))
} else {
currentUser.MessageCh <- "Tên mới không hợp lệ."
}
} else if message == "/who" {
sendUserList(currentUser)
} else {
// Gửi tin nhắn thông thường để phát sóng
messageCh <- fmt.Sprintf("%s: %s", currentUser.Name, message)
}
timeoutCh = time.After(inactivityTimeout) // Reset timeout sau mỗi hoạt động
}
}
}
func main() {
go manager() // Khởi chạy goroutine quản lý phòng chat
listenAddr := "127.0.0.1:8003"
listener, err := net.Listen("tcp", listenAddr)
if err != nil {
fmt.Printf("Lỗi lắng nghe: %v\n", err)
return
}
defer listener.Close()
fmt.Printf("Server phòng chat đang lắng nghe trên %s\n", listenAddr)
for {
conn, err := listener.Accept()
if err != nil {
fmt.Printf("Lỗi chấp nhận kết nối: %v\n", err)
continue
}
go handleClientConnection(conn) // Xử lý mỗi client trong một goroutine riêng
}
}
Gói net/http và máy chủ Web
Go có gói net/http tích hợp sẵn để xây dựng các máy chủ và client HTTP.
1. Tạo máy chủ Web đơn giản
package main
import (
"fmt"
"net/http"
)
func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Chào mừng đến với trang Go!") // Ghi phản hồi HTTP
}
func greetHandler(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name") // Lấy tham số 'name' từ URL
if name == "" {
name = "Khách"
}
fmt.Fprintf(w, "Xin chào, %s!", name)
}
func main() {
// Đăng ký hàm xử lý cho các đường dẫn
http.HandleFunc("/", helloHandler) // Xử lý yêu cầu đến đường dẫn gốc
http.HandleFunc("/greet", greetHandler) // Xử lý yêu cầu đến /greet
listenAddr := "127.0.0.1:8080"
fmt.Printf("Server HTTP đang chạy trên http://%s\n", listenAddr)
// Khởi động server HTTP
err := http.ListenAndServe(listenAddr, nil) // nil nghĩa là sử dụng DefaultServeMux
if err != nil {
fmt.Printf("Lỗi khởi động server: %v\n", err)
}
}
Truy cập http://127.0.0.1:8080/ và http://127.0.0.1:8080/greet?name=Alice trong trình duyệt.
Web Crawler (Trình thu thập dữ liệu Web)
Một web crawler là một chương trình tự động duyệt Internet, thu thập thông tin từ các trang web.
Quy trình cơ bản:
- Xác định URL mục tiêu.
- Gửi yêu cầu HTTP đến URL đó và nhận phản hồi.
- Phân tích cú pháp phản hồi (thường là HTML) để trích xuất thông tin hữu ích và các URL mới.
- Lưu trữ hoặc xử lý dữ liệu đã trích xuất.
- Lặp lại quy trình với các URL mới tìm được.
Ví dụ thu thập dữ liệu cơ bản
package main
import (
"fmt"
"io"
"net/http"
"os"
"regexp"
"strings"
"sync"
"time"
)
// fetchPageContent gửi yêu cầu GET và trả về nội dung trang
func fetchPageContent(url string) (string, error) {
fmt.Printf("Đang tải: %s\n", url)
resp, err := http.Get(url)
if err != nil {
return "", fmt.Errorf("lỗi khi tải URL %s: %v", url, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("URL %s trả về trạng thái lỗi: %d %s", url, resp.StatusCode, resp.Status)
}
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("lỗi khi đọc nội dung phản hồi từ %s: %v", url, err)
}
return string(bodyBytes), nil
}
// saveContentToFile lưu nội dung vào một file
func saveContentToFile(filename, content string) error {
file, err := os.Create(filename)
if err != nil {
return fmt.Errorf("không thể tạo file %s: %v", filename, err)
}
defer file.Close()
_, err = file.WriteString(content)
if err != nil {
return fmt.Errorf("không thể ghi vào file %s: %v", filename, err)
}
fmt.Printf("Đã lưu nội dung vào: %s\n", filename)
return nil
}
// extractLinks tìm các URL từ nội dung HTML
func extractLinks(htmlContent string) []string {
// Biểu thức chính quy đơn giản để tìm các thẻ <a> với href
// (?s:.*?) để khớp với nội dung nhiều dòng
re := regexp.MustCompile(`(?s:]*?\s+)?href="([^"]*)")`)
matches := re.FindAllStringSubmatch(htmlContent, -1)
var links []string
for _, match := range matches {
if len(match) > 1 {
link := match[1]
// Chỉ thêm các link có vẻ hợp lệ và không phải là #neo
if strings.HasPrefix(link, "http") || strings.HasPrefix(link, "/") {
if !strings.Contains(link, "#") {
links = append(links, link)
}
}
}
}
return links
}
// spiderPage thu thập dữ liệu từ một trang duy nhất
func spiderPage(url string, pageNum int, wg *sync.WaitGroup) {
defer wg.Done()
content, err := fetchPageContent(url)
if err != nil {
fmt.Printf("Lỗi thu thập dữ liệu trang %s: %v\n", url, err)
return
}
filename := fmt.Sprintf("page_%d.html", pageNum)
err = saveContentToFile(filename, content)
if err != nil {
fmt.Printf("Lỗi lưu file: %v\n", err)
}
// Ví dụ: tìm các link mới (có thể mở rộng để tiếp tục thu thập)
// foundLinks := extractLinks(content)
// fmt.Printf("Tìm thấy %d liên kết trên trang %d\n", len(foundLinks), pageNum)
// for _, link := range foundLinks {
// fmt.Println(" - " + link)
// }
}
func main() {
baseURL := "https://www.example.com" // Thay thế bằng URL thực tế
numPagesToCrawl := 3
var wg sync.WaitGroup
fmt.Println("Bắt đầu Web Crawler...")
for i := 0; i < numPagesToCrawl; i++ {
pageURL := fmt.Sprintf("%s/page/%d", baseURL, i+1) // Tạo URL cho các trang giả định
wg.Add(1)
go spiderPage(pageURL, i+1, &wg) // Khởi chạy goroutine cho mỗi trang
time.Sleep(100 * time.Millisecond) // Tránh quá tải server
}
wg.Wait() // Chờ tất cả goroutine hoàn thành
fmt.Println("Web Crawler hoàn tất.")
}
Sử dụng Biểu thức chính quy (Regular Expressions)
Gói regexp trong Go cho phép làm việc với biểu thức chính quy để tìm kiếm và trích xuất dữ liệu.
regexp.MustCompile(pattern string) *Regexp: Biên dịch một biểu thức chính quy. Nếu có lỗi, nó sẽ panic.regexp.MustCompile(pattern string) *Regexp: Biên dịch một biểu thức chính quy. Nếu có lỗi, nó sẽ panic.re.FindAllStringSubmatch(s string, n int) [][]string: Tìm tất cả các kết quả khớp trong chuỗis.nlà số lượng kết quả tối đa (-1để tìm tất cả). Trả về một slice chứa các slice chuỗi, vớimatch[0]là toàn bộ kết quả khớp vàmatch[1],match[2], ... là các nhóm con (capturing groups).(?s:.*?): Một pattern hữu ích để khớp với nội dung nhiều dòng.(?s)bật cờ "dotall" (dấu chấm khớp với ký tự ngắt dòng), và.*?là non-greedy match.
Thu thập dữ liệu hai chiều
- Theo chiều ngang (Horizontal): Thu thập từng trang web theo thứ tự (ví dụ: trang 1, trang 2, ...).
- Theo chiều dọc (Vertical): Thu thập tất cả các mục hoặc thông tin chi tiết trên một trang cụ thể.