Featured image of post Một dòng chú thích đặc biệt - đỏ 99 vip

Một dòng chú thích đặc biệt - đỏ 99 vip

Trải nghiệm đẳng cấp với đỏ 99 vip - Đỏ 99 vip mang lại những trải nghiệm đặc biệt nhất.

25 tháng 12 năm 2020 Trần Hạo Bình luận 18 bình luận 53.040 người đọc

Vấn đề chính mà lập trình generic muốn giải quyết là sự kết hợp chặt chẽ giữa kiểu dữ liệu và thuật toán xử lý. Khi làm việc với ngôn ngữ có kiểu tĩnh, các thuật toán hoặc chương trình xử lý dữ liệu thường cần được sao chép cho mỗi kiểu dữ liệu khác nhau. Điều này dẫn đến sự phức tạp và khó bảo trì trong mã nguồn. Generic programming giúp giảm thiểu điều này bằng cách cho phép bạn viết mã mà không cần lo lắng về loại dữ liệu cụ thể đang được xử lý, thay vào đó chỉ tập trung vào logic xử lý.

  • Mẫu thiết kế lập trình Go: Slices, Interface, Thời gian và Hiệu suất
    • Mẫu thiết kế lập trình Go: Xử lý lỗi
    • Mẫu thiết kế lập trình Go: Functional Options
    • Mẫu thiết kế lập trình Go: Delegation và Inversion of Control
    • Mẫu thiết kế lập trình Go: Map-Reduce
    • Mẫu thiết kế lập trình Go: Code Generation
    • Mẫu thiết kế lập trình Go: Decorator
    • Mẫu thiết kế lập trình Go: Pipeline
    • Mẫu thiết kế lập trình Go: Visitor Pattern trong Kubernetes
    • Mẫu thiết kế lập trình Go: Generic Programming

« Bài viết trước | Bài viết sau »

MỤC LỤC

  • So sánh thực tế
  • Kiểm tra kiểu trong ngôn ngữ Go
    • Type Assertion
    • Reflection
  • Học hỏi từ các ngôn ngữ khác
  • Generator trong Go
    • Mẫu hàm
    • Kịch bản tạo mã
    • Tạo mã nguồn
  • Filter mới
  • Công cụ bên thứ ba

So sánh thực tế

Hãy lấy ví dụ về tua vít để minh họa vấn đề. Một cây tua vít cơ bản thực hiện công việc vặn bu lông, nhưng vì có nhiều loại bu lông khác nhau (bình thường, chữ thập, lục giác…) và kích thước khác nhau, nên chúng ta cần nhiều loại tua vít khác nhau để đáp ứng nhu cầu.

Thực tế, thay vì tạo ra nhiều loại tua vít riêng biệt, chúng ta có thể thiết kế một công cụ thông minh hơn, có khả năng tự điều chỉnh để phù hợp với nhiều loại bu lông khác nhau. Đây chính là mục tiêu mà lập trình generic hướng tới: tập trung vào chức năng cốt lõi của công cụ thay vì phải quan tâm quá nhiều đến đặc điểm cụ thể của đối tượng cần xử lý.

Kiểm tra kiểu trong ngôn ngữ Go

Hiện tại, Go chưa hỗ trợ đầy đủ tính năng generic. Do đó, để khắc phục hạn chế này, chúng ta thường sử dụng interface{} - một kiểu dữ liệu tổng quát tương tự như void* trong C/C++. Tuy nhiên, việc này đòi hỏi phải kiểm tra kiểu dữ liệu trong quá trình thực thi. Trong Go, có hai kỹ thuật phổ biến để kiểm tra kiểu: Type Assertion và Reflection.

Type Assertion

Type Assertion là kỹ thuật dùng để chuyển đổi kiểu dữ liệu của một biến từ interface{} sang kiểu cụ thể. Kết quả trả về bao gồm hai giá trị: giá trị đã được chuyển đổi và một boolean cho biết quá trình chuyển đổi có thành công hay không.

Dưới đây là một ví dụ về cách sử dụng Type Assertion:

1
2
3
4
5
6
// Kiểm tra kiểu dữ liệu thực tế là int
elem, ok := intContainer.Get().(int)
if !ok {
    fmt.Println("Không thể đọc kiểu int từ intContainer")
}
fmt.Printf("assertExample: %d (%T)\n", elem, elem)
Reflection

Reflection là kỹ thuật mạnh mẽ hơn, cho phép kiểm tra và thao tác với cấu trúc của đối tượng tại thời gian chạy. Dưới đây là một ví dụ về cách sử dụng reflection trong Go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
type Container struct {
    s reflect.Value
}

func NewContainer(t reflect.Type, size int) *Container {
    if size <= 0 {
        size = 64
    }
    return &Container{
        s: reflect.MakeSlice(reflect.SliceOf(t), 0, size),
    }
}

func (c *Container) Put(val interface{}) error {
    if reflect.ValueOf(val).Type() != c.s.Type().Elem() {
        return fmt.Errorf("Put: không thể thêm kiểu %T vào slice của %s", val, c.s.Type().Elem())
    }
    c.s = reflect.Append(c.s, reflect.ValueOf(val))
    return nil
}

func (c *Container) Get(refval interface{}) error {
    if reflect.ValueOf(refval).Kind() != reflect.Ptr || reflect.ValueOf(refval).Elem().Type() != c.s.Type().Elem() {
        return fmt.Errorf("Get: cần *%s nhưng nhận được %T", c.s.Type().Elem(), refval)
    }
    reflect.ValueOf(refval).Elem().Set(c.s.Index(0))
    c.s = c.s.Slice(1, c.s.Len())
    return nil
}

Trong đoạn mã trên:

  • Hàm NewContainer() khởi tạo một slice dựa trên kiểu dữ liệu được chỉ định.
  • Hàm Put() kiểm tra xem giá trị cần thêm có đúng kiểu với slice hay không.
  • Hàm Get() yêu cầu truyền tham chiếu để lưu trữ giá trị được lấy ra, do không thể trả về trực tiếp reflect.Value hoặc interface{}.
Sử dụng reflection
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
f1 := 3.1415926
f2 := 1.41421356237
c := NewMyContainer(reflect.TypeOf(f1), 16)
if err := c.Put(f1); err != nil {
    panic(err)
}
if err := c.Put(f2); err != nil {
    panic(err)
}
g := 0.0
if err := c.Get(&g); err != nil {
    panic(err)
}
fmt.Printf("%v (%T)\n", g, g) //3.1415926 (float64)
fmt.Println(c.s.Index(0)) //1.4142135623

Reflection mặc dù mạnh mẽ nhưng lại khiến mã nguồn trở nên phức tạp hơn. Vậy có cách nào đơn giản hơn không?

Học hỏi từ các ngôn ngữ khác

Trong C++, vấn đề generic được giải quyết bằng cách sử dụng Template. Ví dụ:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
template <class T> 
T GetMax (T a, T b) { 
    T result; 
    result = (a>b)? a : b; 
    return (result); 
} 

int i=5, j=6, k; 
k=GetMax<int>(i,j); // Tự động tạo hàm với kiểu int

long l=10, m=5, n; 
n=GetMax<long>(l,m); // Tự động tạo hàm với kiểu long

Compiler của C++ sẽ tự động tạo ra các phiên bản hàm hoặc lớp theo từng kiểu dữ liệu khác nhau mà không cần bất kỳ kiểm tra kiểu nào tại thời gian chạy. Mã nguồn trở nên sạch sẽ và dễ bảo trì hơn.

Tương tự, chúng ta cũng có thể áp dụng kỹ thuật này trong Go bằng cách sử dụng code generation.

Generator trong Go

Để tạo mã nguồn trong Go, bạn cần ba bước chính:

  1. Một mẫu hàm chứa các placeholder.
  2. Một kịch bản tự động thay thế placeholder và tạo mã mới.
  3. Một dòng chú thích đặc biệt.
Mẫu hàm

Chúng ta có thể viết một mẫu hàm như sau và lưu nó dưới tên container.tmp.go trong thư mục ./template/:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package PACKAGE_NAME
type GENERIC_NAMEContainer struct {
    s []GENERIC_TYPE
}
func NewGENERIC_NAMEContainer() *GENERIC_NAMEContainer {
    return &GENERIC_NAMEContainer{s: []GENERIC_TYPE{}}
}
func (c *GENERIC_NAMEContainer) Put(val GENERIC_TYPE) {
    c.s = append(c.s, val)
}
func (c *GENERIC_NAMEContainer) Get() GENERIC_TYPE {
    r := c.s[0]
    c.s = c.s[1:]
    return r
}

Placeholder bao gồm:

  • PACKAGE_NAME – Tên gói
  • GENERIC_NAME – Tên đại diện
  • GENERIC_TYPE – Kiểu dữ liệu cụ thể
Kịch bản tạo mã

Tiếp theo, chúng ta tạo một script shbet love gọi là game nổ hũ đăng ký tặng code gen.sh để tự động thay thế placeholder:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#!/bin/bash
set -e
SRC_FILE=${1}
PACKAGE=${2}
TYPE=${3}
DES=${4}
# Chuyển ký tự đầu tiên thành chữ hoa
PREFIX="$(tr '[:lower:]' '[:upper:]' <<< ${TYPE:0:1})${TYPE:1}"
DES_FILE=$(echo ${TYPE}| tr '[:upper:]' '[:lower:]')_${DES}.go
sed 's/PACKAGE_NAME/'"${PACKAGE}"'/g' ${SRC_FILE} | \
  sed 's/GENERIC_TYPE/'"${TYPE}"'/g' | \
  sed 's/GENERIC_NAME/'"${PREFIX}"'/g' > ${DES_FILE}

Script này nhận bốn tham số:

  • File mẫu nguồn
  • Tên gói
  • Kiểu dữ liệu cụ thể
  • Phần mở rộng tên file đích
Tạo mã nguồn

Cuối cùng, chúng ta chỉ cần thêm một dòng chú thích đặc biệt trong mã nguồn:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
//go:generate ./gen.sh ./template/container.tmp.go gen uint32 container
func generateUint32Example() {
    var u uint32 = 42
    c := NewUint32Container()
    c.Put(u)
    v := c.Get()
    fmt.Printf("generateExample: %d (%T)\n", v, v)
}

//go:generate ./gen.sh ./template/container.tmp.go [Xeng Club Top 5 Game Bài Đổi Thưởng](/uo/22320.html)  gen string container
func generateStringExample() {
    var s string = "Hello"
    c := NewStringContainer()
    c.Put(s)
    v := c.Get()
    fmt.Printf("generateExample: %s (%T)\n", v, v)
}

Sau đó, chạy lệnh go generate trong thư mục dự án, bạn sẽ nhận được hai file mới:

File uint32_container.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package gen
type Uint32Container struct {
    s []uint32
}
func NewUint32Container() *Uint32Container {
    return &Uint32Container{s: []uint32{}}
}
func (c *Uint32Container) Put(val uint32) {
    c.s = append(c.s, val)
}
func (c *Uint32Container) Get() uint32 {
    r := c.s[0]
    c.s = c.s[1:]
    return r
}

File string_container.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package gen
type StringContainer struct {
    s []string
}
func NewStringContainer() *StringContainer {
    return &StringContainer{s: []string{}}
}
func (c *StringContainer) Put(val string) {
    c.s = append(c.s, val)
}
func (c *StringContainer) Get() string {
    r := c.s[0]
    c.s = c.s[1:]
    return r
}

Hai file này đảm bảo rằng mã nguồn hoàn toàn biên dịch được, với chi phí duy nhất là phải chạy thêm lệnh go generate.

Filter mới

Bằng cách áp dụng kỹ thuật này, chúng ta có thể viết mã nguồn sạch hơn mà không cần sử dụng reflection phức tạp. Dưới đây là một ví dụ về filter:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package PACKAGE_NAME
type GENERIC_NAMEList []GENERIC_TYPE
type GENERIC_NAMEToBool func(*GENERIC_TYPE) bool
func (al GENERIC_NAMEList) Filter(f GENERIC_NAMEToBool) GENERIC_NAMEList {
    var ret GENERIC_NAMEList
    for _, a := range al {
        if f(&a) {
            ret = append(ret, a)
        }
    }
    return ret
}
Sử dụng filter
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
type Employee struct {
    Name     string
    Age      int
    Vacation int
    Salary   int
}

//go:generate ./gen.sh ./template/filter.tmp.go gen Employee filter
func filterEmployeeExample() {
    var list = EmployeeList{
        {"Hao", 44, 0, 8000},
        {"Bob", 34, 10, 5000},
        {"Alice", 23, 5, 9000},
        {"Jack", 26, 0, 4000},
        {"Tom", 48, 9, 7500},
    }
    var filter EmployeeList
    filter = list.Filter(func(e *Employee) bool {
        return e.Age > 40
    })
    fmt.Println("----- Employee.Age > 40 ------")
    for _, e := range filter {
        fmt.Println(e)
    }

    filter = list.Filter(func(e *Employee) bool {
        return e.Salary <= 5000
    })
    fmt.Println("----- Employee.Salary <= 5000 ------")
    for _, e := range filter {
        fmt.Println(e)
    }
}

Công cụ bên thứ ba

Ngoài việc tự viết script, bạn có thể sử dụng các công cụ sẵn có từ cộng đồng:

  • Genny
  • Generic
  • GenGen
  • Gen
Built with Hugo
Theme Stack thiết kế bởi Jimmy