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

Method và interface

Bản dịch từ go.dev/tour/methods/1

Method

Go không có class. Nhưng bạn có thể định nghĩa method trên các kiểu dữ liệu.

Method là một hàm có thêm một đối số đặc biệt gọi là receiver (giá trị mà method gắn vào).

Receiver nằm trong danh sách đối số riêng của nó, đặt giữa từ khoá func và tên method.

Trong ví dụ này, method Abs có một receiver kiểu Vertex tên là v.

package main

import (
	"fmt"
	"math"
)

type Vertex struct {
	X, Y float64
}

func (v Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
	v := Vertex{3, 4}
	fmt.Println(v.Abs())
}

Chạy thử ví dụ này trong Tour of Go ↗

Method cũng chỉ là hàm

Hãy nhớ: method chỉ là một hàm có thêm đối số receiver.

Dưới đây là Abs viết lại thành một hàm thông thường, chức năng không đổi.

package main

import (
	"fmt"
	"math"
)

type Vertex struct {
	X, Y float64
}

func Abs(v Vertex) float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
	v := Vertex{3, 4}
	fmt.Println(Abs(v))
}

Chạy thử ví dụ này trong Tour of Go ↗

Method (tiếp)

Bạn cũng có thể khai báo method trên các kiểu không phải struct.

Trong ví dụ này ta thấy một kiểu số MyFloat có method Abs.

Bạn chỉ có thể khai báo method với receiver có kiểu được định nghĩa trong cùng package với method đó. Bạn không thể khai báo method với receiver có kiểu được định nghĩa ở package khác (kể cả các kiểu dựng sẵn như int).

package main

import (
	"fmt"
	"math"
)

type MyFloat float64

func (f MyFloat) Abs() float64 {
	if f < 0 {
		return float64(-f)
	}
	return float64(f)
}

func main() {
	f := MyFloat(-math.Sqrt2)
	fmt.Println(f.Abs())
}

Chạy thử ví dụ này trong Tour of Go ↗

Pointer receiver

Bạn có thể khai báo method với pointer receiver.

Nghĩa là kiểu của receiver được viết theo cú pháp *T với một kiểu T nào đó. (Ngoài ra, bản thân T không được là một pointer, ví dụ *int.)

Chẳng hạn, method Scale ở đây được định nghĩa trên *Vertex.

Method có pointer receiver thì sửa được giá trị mà receiver trỏ tới (đúng như Scale làm ở đây). Vì method thường cần sửa receiver của mình, pointer receiver phổ biến hơn value receiver.

Hãy thử bỏ dấu * trong khai báo của hàm Scale ở dòng 16 và xem hành vi của chương trình thay đổi ra sao.

Với value receiver, method Scale chỉ làm việc trên một bản sao của giá trị Vertex gốc. (Điều này giống hệt như với mọi đối số hàm khác.) Method Scale phải có pointer receiver thì mới thay đổi được giá trị Vertex đã khai báo trong hàm main.

package main

import (
	"fmt"
	"math"
)

type Vertex struct {
	X, Y float64
}

func (v Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func (v *Vertex) Scale(f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}

func main() {
	v := Vertex{3, 4}
	v.Scale(10)
	fmt.Println(v.Abs())
}

Chạy thử ví dụ này trong Tour of Go ↗

Pointer và hàm

Ở đây ta thấy hai method Abs và Scale được viết lại thành hàm.

Một lần nữa, hãy thử bỏ dấu * ở dòng 16. Bạn có thấy vì sao hành vi thay đổi không? Bạn còn phải sửa gì nữa để ví dụ biên dịch được?

(Nếu chưa chắc, hãy đọc tiếp trang sau.)

package main

import (
	"fmt"
	"math"
)

type Vertex struct {
	X, Y float64
}

func Abs(v Vertex) float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func Scale(v *Vertex, f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}

func main() {
	v := Vertex{3, 4}
	Scale(&v, 10)
	fmt.Println(Abs(v))
}

Chạy thử ví dụ này trong Tour of Go ↗

Method và việc tự động chuyển đổi pointer

So sánh hai chương trình vừa rồi, có thể bạn nhận ra rằng hàm nhận đối số kiểu pointer thì bắt buộc phải được truyền pointer:

var v Vertex
ScaleFunc(v, 5)  // Compile error!
ScaleFunc(&v, 5) // OK

còn method có pointer receiver thì khi gọi có thể nhận receiver là giá trị hoặc pointer đều được:

var v Vertex
v.Scale(5)  // OK
p := &v
p.Scale(10) // OK

Với câu lệnh v.Scale(5), dù v là một giá trị chứ không phải pointer, method có pointer receiver vẫn được gọi một cách tự động. Cụ thể, để cho tiện, Go hiểu câu lệnh v.Scale(5) thành (&v).Scale(5), vì method Scale có pointer receiver.

package main

import "fmt"

type Vertex struct {
	X, Y float64
}

func (v *Vertex) Scale(f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}

func ScaleFunc(v *Vertex, f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}

func main() {
	v := Vertex{3, 4}
	v.Scale(2)
	ScaleFunc(&v, 10)

	p := &Vertex{4, 3}
	p.Scale(3)
	ScaleFunc(p, 8)

	fmt.Println(v, p)
}

Chạy thử ví dụ này trong Tour of Go ↗

Method và việc tự động chuyển đổi pointer (2)

Chiều ngược lại cũng có điều tương tự.

Hàm nhận đối số là giá trị thì bắt buộc phải được truyền giá trị đúng kiểu đó:

var v Vertex
fmt.Println(AbsFunc(v))  // OK
fmt.Println(AbsFunc(&v)) // Compile error!

còn method có value receiver thì khi gọi có thể nhận receiver là giá trị hoặc pointer đều được:

var v Vertex
fmt.Println(v.Abs()) // OK
p := &v
fmt.Println(p.Abs()) // OK

Trong trường hợp này, lời gọi method p.Abs() được hiểu thành (*p).Abs().

package main

import (
	"fmt"
	"math"
)

type Vertex struct {
	X, Y float64
}

func (v Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func AbsFunc(v Vertex) float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
	v := Vertex{3, 4}
	fmt.Println(v.Abs())
	fmt.Println(AbsFunc(v))

	p := &Vertex{4, 3}
	fmt.Println(p.Abs())
	fmt.Println(AbsFunc(*p))
}

Chạy thử ví dụ này trong Tour of Go ↗

Chọn value receiver hay pointer receiver

Có hai lý do để dùng pointer receiver.

Thứ nhất, để method có thể sửa được giá trị mà receiver trỏ tới.

Thứ hai, để tránh sao chép giá trị ở mỗi lần gọi method. Cách này hiệu quả hơn khi receiver là một struct lớn chẳng hạn.

Trong ví dụ này, cả Scale lẫn Abs đều là method có receiver kiểu *Vertex, dù method Abs không cần sửa receiver của nó.

Nói chung, mọi method của một kiểu nên dùng đồng nhất value receiver hoặc pointer receiver, chứ không trộn lẫn cả hai. (Vài trang tới ta sẽ thấy vì sao.)

package main

import (
	"fmt"
	"math"
)

type Vertex struct {
	X, Y float64
}

func (v *Vertex) Scale(f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}

func (v *Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
	v := &Vertex{3, 4}
	fmt.Printf("Before scaling: %+v, Abs: %v\n", v, v.Abs())
	v.Scale(5)
	fmt.Printf("After scaling: %+v, Abs: %v\n", v, v.Abs())
}

Chạy thử ví dụ này trong Tour of Go ↗

Interface

Một kiểu_interface được định nghĩa là một tập các chữ ký method.

Giá trị thuộc kiểu interface có thể chứa bất kỳ giá trị nào hiện thực các method đó.

Lưu_ý: Có một lỗi trong mã ví dụ ở dòng 22. Vertex (kiểu giá trị) không hiện thực Abser, vì method Abs chỉ được định nghĩa trên *Vertex (kiểu pointer).

package main

import (
	"fmt"
	"math"
)

type Abser interface {
	Abs() float64
}

func main() {
	var a Abser
	f := MyFloat(-math.Sqrt2)
	v := Vertex{3, 4}

	a = f  // a MyFloat implements Abser
	a = &v // a *Vertex implements Abser

	// In the following line, v is a Vertex (not *Vertex)
	// and does NOT implement Abser.
	a = v

	fmt.Println(a.Abs())
}

type MyFloat float64

func (f MyFloat) Abs() float64 {
	if f < 0 {
		return float64(-f)
	}
	return float64(f)
}

type Vertex struct {
	X, Y float64
}

func (v *Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

Chạy thử ví dụ này trong Tour of Go ↗

Interface được hiện thực một cách ngầm định

Một kiểu hiện thực một interface bằng cách hiện thực các method của interface đó. Không cần khai báo ý định một cách tường minh, không có từ khoá “implements”.

Interface ngầm định tách rời phần định nghĩa interface khỏi phần hiện thực nó, nhờ vậy phần hiện thực có thể nằm ở bất kỳ package nào mà không cần thoả thuận trước.

package main

import "fmt"

type I interface {
	M()
}

type T struct {
	S string
}

// This method means type T implements the interface I,
// but we don't need to explicitly declare that it does so.
func (t T) M() {
	fmt.Println(t.S)
}

func main() {
	var i I = T{"hello"}
	i.M()
}

Chạy thử ví dụ này trong Tour of Go ↗

Giá trị interface

Bên dưới lớp vỏ, có thể hình dung giá trị interface như một cặp gồm một giá trị và một kiểu cụ thể:

(value, type)

Một giá trị interface chứa một giá trị thuộc kiểu cụ thể nằm bên dưới nó.

Gọi một method trên giá trị interface sẽ chạy method cùng tên của kiểu cụ thể nằm bên dưới.

package main

import (
	"fmt"
	"math"
)

type I interface {
	M()
}

type T struct {
	S string
}

func (t *T) M() {
	fmt.Println(t.S)
}

type F float64

func (f F) M() {
	fmt.Println(f)
}

func main() {
	var i I

	i = &T{"Hello"}
	describe(i)
	i.M()

	i = F(math.Pi)
	describe(i)
	i.M()
}

func describe(i I) {
	fmt.Printf("(%v, %T)\n", i, i)
}

Chạy thử ví dụ này trong Tour of Go ↗

Giá trị interface chứa giá trị nil bên dưới

Nếu giá trị cụ thể bên trong interface là nil, method sẽ được gọi với receiver là nil.

Ở một số ngôn ngữ, điều này gây ra lỗi null pointer exception, nhưng trong Go, viết method xử lý êm thấm trường hợp receiver là nil là chuyện bình thường (như method M trong ví dụ này).

Lưu ý rằng một giá trị interface đang chứa một giá trị cụ thể bằng nil thì bản thân nó vẫn khác nil.

package main

import "fmt"

type I interface {
	M()
}

type T struct {
	S string
}

func (t *T) M() {
	if t == nil {
		fmt.Println("<nil>")
		return
	}
	fmt.Println(t.S)
}

func main() {
	var i I

	var t *T
	i = t
	describe(i)
	i.M()

	i = &T{"hello"}
	describe(i)
	i.M()
}

func describe(i I) {
	fmt.Printf("(%v, %T)\n", i, i)
}

Chạy thử ví dụ này trong Tour of Go ↗

Giá trị interface bằng nil

Một giá trị interface bằng nil thì không chứa giá trị, cũng không chứa kiểu cụ thể nào.

Gọi method trên một interface bằng nil là lỗi lúc chạy, vì không có kiểu nào trong cặp giá trị-kiểu của interface để biết phải gọi method cụ_thể nào.

package main

import "fmt"

type I interface {
	M()
}

func main() {
	var i I
	describe(i)
	i.M()
}

func describe(i I) {
	fmt.Printf("(%v, %T)\n", i, i)
}

Chạy thử ví dụ này trong Tour of Go ↗

Interface rỗng

Kiểu interface không có method nào được gọi là interface_rỗng:

interface{}

Interface rỗng có thể chứa giá trị thuộc bất kỳ kiểu nào. (Mọi kiểu đều hiện thực ít nhất là không method nào.)

any là tên gọi khác của interface{}, hai cách viết hoàn toàn tương đương nhau.

Interface rỗng được dùng trong mã xử lý những giá trị chưa biết kiểu. Ví dụ, fmt.Print nhận bao nhiêu đối số kiểu any cũng được.

package main

import "fmt"

func main() {
	var i interface{}
	describe(i)

	i = 42
	describe(i)

	i = "hello"
	describe(i)
}

func describe(i interface{}) {
	fmt.Printf("(%v, %T)\n", i, i)
}

Chạy thử ví dụ này trong Tour of Go ↗

Type assertion

Type_assertion cho bạn truy cập vào giá trị cụ thể nằm bên dưới một giá trị interface.

t := i.(T)

Câu lệnh này khẳng định rằng giá trị interface i đang chứa kiểu cụ thể T rồi gán giá trị T bên dưới cho biến t.

Nếu i không chứa một T, câu lệnh sẽ gây panic.

Để kiểm_tra xem một giá trị interface có chứa một kiểu nào đó hay không, type assertion có thể trả về hai giá trị: giá trị nằm bên dưới và một giá trị boolean cho biết phép khẳng định có thành công không.

t, ok := i.(T)

Nếu i chứa một T thì t sẽ là giá trị bên dưới còn ok sẽ là true.

Nếu không, ok sẽ là false và t sẽ là giá trị zero của kiểu T, và không có panic nào xảy ra.

Hãy để ý cú pháp này giống với cú pháp đọc giá trị từ một map.

package main

import "fmt"

func main() {
	var i interface{} = "hello"

	s := i.(string)
	fmt.Println(s)

	s, ok := i.(string)
	fmt.Println(s, ok)

	f, ok := i.(float64)
	fmt.Println(f, ok)

	f = i.(float64) // panic
	fmt.Println(f)
}

Chạy thử ví dụ này trong Tour of Go ↗

Type switch

Type_switch là cấu trúc cho phép thực hiện nhiều type assertion nối tiếp nhau.

Type switch trông giống câu lệnh switch thông thường, nhưng các case trong type switch chỉ định kiểu (chứ không phải giá trị), và các kiểu đó được so với kiểu của giá trị mà giá trị interface đang chứa.

switch v := i.(type) {
case T:
	// here v has type T
case S:
	// here v has type S
default:
	// no match; here v has the same type as i
}

Phần khai báo trong type switch có cú pháp giống type assertion i.(T), chỉ khác là kiểu cụ thể T được thay bằng từ khoá type.

Câu lệnh switch này kiểm tra xem giá trị interface i có chứa giá trị kiểu T hay S không. Ở mỗi case T và S, biến v sẽ có kiểu lần lượt là T hoặc S và mang giá trị mà i đang chứa. Ở case default (khi không khớp kiểu nào), biến v có cùng kiểu interface và cùng giá trị với i.

package main

import "fmt"

func do(i interface{}) {
	switch v := i.(type) {
	case int:
		fmt.Printf("Twice %v is %v\n", v, v*2)
	case string:
		fmt.Printf("%q is %v bytes long\n", v, len(v))
	default:
		fmt.Printf("I don't know about type %T!\n", v)
	}
}

func main() {
	do(21)
	do("hello")
	do(true)
}

Chạy thử ví dụ này trong Tour of Go ↗

Stringer

Một trong những interface phổ biến nhất là Stringer, được định nghĩa trong package fmt.

type Stringer interface {
	String() string
}

Stringer là kiểu có thể tự mô tả mình bằng một chuỗi. Package fmt (và nhiều package khác) tìm interface này khi in giá trị ra.

package main

import "fmt"

type Person struct {
	Name string
	Age  int
}

func (p Person) String() string {
	return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
}

func main() {
	a := Person{"Arthur Dent", 42}
	z := Person{"Zaphod Beeblebrox", 9001}
	fmt.Println(a, z)
}

Chạy thử ví dụ này trong Tour of Go ↗

Bài tập: Stringer

Hãy làm cho kiểu IPAddr hiện thực fmt.Stringer để in địa chỉ ra dưới dạng bốn số ngăn cách bằng dấu chấm.

Ví dụ, IPAddr{1, 2, 3, 4} phải in ra "1.2.3.4".

package main

import "fmt"

type IPAddr [4]byte

// TODO: Add a "String() string" method to IPAddr.

func main() {
	hosts := map[string]IPAddr{
		"loopback":  {127, 0, 0, 1},
		"googleDNS": {8, 8, 8, 8},
	}
	for name, ip := range hosts {
		fmt.Printf("%v: %v\n", name, ip)
	}
}

Chạy thử ví dụ này trong Tour of Go ↗

Lỗi

Chương trình Go biểu diễn trạng thái lỗi bằng các giá trị error.

Kiểu error là một interface dựng sẵn, tương tự fmt.Stringer:

type error interface {
	Error() string
}

(Cũng như với fmt.Stringer, package fmt tìm interface error khi in giá trị ra.)

Các hàm thường trả về một giá trị error, và mã gọi hàm nên xử lý lỗi bằng cách kiểm tra xem lỗi có bằng nil hay không.

i, err := strconv.Atoi("42")
if err != nil {
	fmt.Printf("couldn't convert number: %v\n", err)
	return
}
fmt.Println("Converted integer:", i)

error bằng nil nghĩa là thành công; error khác nil nghĩa là thất bại.

package main

import (
	"fmt"
	"time"
)

type MyError struct {
	When time.Time
	What string
}

func (e *MyError) Error() string {
	return fmt.Sprintf("at %v, %s",
		e.When, e.What)
}

func run() error {
	return &MyError{
		time.Now(),
		"it didn't work",
	}
}

func main() {
	if err := run(); err != nil {
		fmt.Println(err)
	}
}

Chạy thử ví dụ này trong Tour of Go ↗

Bài tập: Lỗi

Hãy chép lại hàm Sqrt của bạn từ bài tập trước và sửa nó để trả về thêm một giá trị error.

Sqrt phải trả về một giá trị error khác nil khi nhận số âm, vì nó không hỗ trợ số phức.

Hãy tạo một kiểu mới

type ErrNegativeSqrt float64

rồi biến nó thành một error bằng cách viết cho nó method

func (e ErrNegativeSqrt) Error() string

sao cho ErrNegativeSqrt(-2).Error() trả về "cannot Sqrt negative number:-2”`.

Lưu_ý: Gọi fmt.Sprint(e) bên trong method Error sẽ khiến chương trình rơi vào vòng lặp vô hạn. Bạn tránh được điều đó bằng cách chuyển kiểu e trước: fmt.Sprint(float64(e)). Vì sao vậy?

Hãy sửa hàm Sqrt của bạn để trả về một giá trị ErrNegativeSqrt khi nhận số âm.

package main

import (
	"fmt"
)

func Sqrt(x float64) (float64, error) {
	return 0, nil
}

func main() {
	fmt.Println(Sqrt(2))
	fmt.Println(Sqrt(-2))
}

Chạy thử ví dụ này trong Tour of Go ↗

Reader

Package io định nghĩa interface io.Reader, đại diện cho đầu đọc của một luồng dữ liệu.

Thư viện chuẩn của Go chứa rất nhiều hiện thực của interface này, gồm tệp, kết nối mạng, bộ nén, bộ mã hoá và nhiều thứ khác.

Interface io.Reader có một method Read:

func (T) Read(b []byte) (n int, err error)

Read đổ dữ liệu vào slice byte được truyền vào rồi trả về số byte đã đổ cùng một giá trị lỗi. Nó trả về lỗi io.EOF khi luồng dữ liệu kết thúc.

Mã ví dụ tạo ra một strings.Reader rồi đọc dần kết quả, mỗi lần 8 byte.

package main

import (
	"fmt"
	"io"
	"strings"
)

func main() {
	r := strings.NewReader("Hello, Reader!")

	b := make([]byte, 8)
	for {
		n, err := r.Read(b)
		fmt.Printf("n = %v err = %v b = %v\n", n, err, b)
		fmt.Printf("b[:n] = %q\n", b[:n])
		if err == io.EOF {
			break
		}
	}
}

Chạy thử ví dụ này trong Tour of Go ↗

Bài tập: Reader

Hãy hiện thực một kiểu Reader phát ra một luồng vô hạn ký tự ASCII 'A'.

package main

import "golang.org/x/tour/reader"

type MyReader struct{}

// TODO: Add a Read([]byte) (int, error) method to MyReader.

func main() {
	reader.Validate(MyReader{})
}

Chạy thử ví dụ này trong Tour of Go ↗

Bài tập: rot13Reader

Một khuôn mẫu quen thuộc là một io.Reader bọc quanh một io.Reader khác và biến đổi luồng dữ liệu theo cách nào đó.

Ví dụ, hàm gzip.NewReader nhận một io.Reader (luồng dữ liệu đã nén) và trả về một *gzip.Reader cũng hiện thực io.Reader (luồng dữ liệu đã giải nén).

Hãy hiện thực một rot13Reader hiện thực io.Reader và đọc từ một io.Reader, biến đổi luồng dữ liệu bằng cách áp dụng phép thay thế rot13 cho mọi ký tự chữ cái.

Kiểu rot13Reader đã được viết sẵn cho bạn. Việc của bạn là biến nó thành một io.Reader bằng cách hiện thực method Read.

package main

import (
	"io"
	"os"
	"strings"
)

type rot13Reader struct {
	r io.Reader
}

func main() {
	s := strings.NewReader("Lbh penpxrq gur pbqr!")
	r := rot13Reader{s}
	io.Copy(os.Stdout, &r)
}

Chạy thử ví dụ này trong Tour of Go ↗

Ảnh

Package image định nghĩa interface Image:

package image

type Image interface {
	ColorModel() color.Model
	Bounds() Rectangle
	At(x, y int) color.Color
}

Lưu_ý: giá trị trả về Rectangle của method Bounds thực chất là một image.Rectangle, vì phần khai báo nằm bên trong package image.

(Xem tài liệu để biết mọi chi tiết.)

Hai kiểu color.Color và color.Model cũng là interface, nhưng ta sẽ tạm bỏ qua điều đó bằng cách dùng các hiện thực có sẵn là color.RGBA và color.RGBAModel. Các interface và kiểu này được định nghĩa trong package image/color.

package main

import (
	"fmt"
	"image"
)

func main() {
	m := image.NewRGBA(image.Rect(0, 0, 100, 100))
	fmt.Println(m.Bounds())
	fmt.Println(m.At(0, 0).RGBA())
}

Chạy thử ví dụ này trong Tour of Go ↗

Bài tập: Ảnh

Bạn còn nhớ bộ sinh ảnh đã viết trước đây chứ? Hãy viết thêm một bộ nữa, nhưng lần này nó trả về một hiện thực của image.Image thay vì một slice dữ liệu.

Hãy định nghĩa kiểu Image của riêng bạn, hiện thực các method cần thiết, rồi gọi pic.ShowImage.

Bounds phải trả về một image.Rectangle, chẳng hạn image.Rect(0, 0, w, h).

ColorModel phải trả về color.RGBAModel.

At phải trả về một màu; giá trị v trong bộ sinh ảnh trước tương ứng với color.RGBA{v, v, 255, 255} ở bài này.

package main

import "golang.org/x/tour/pic"

type Image struct{}

func main() {
	m := Image{}
	pic.ShowImage(m)
}

Chạy thử ví dụ này trong Tour of Go ↗

Chúc mừng!

Bạn đã hoàn thành bài học này!

Bạn có thể quay lại danh sách các module để chọn nội dung học tiếp, hoặc đi tiếp sang bài học sau.

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