- vừa được xem lúc

Sự khác nhau giữa Struct và Interface trong Go

0 0 2

Người đăng: Sang Le

Theo Viblo Asia

1. Bản chất của struct trong Go

1.1 Định nghĩa và vai trò cơ bản

  • struct là kiểu dữ liệu composite cho phép gom nhóm nhiều trường (fields) vào một khối chung, mỗi trường có thể là bất kỳ kiểu nào (int, string, slice, map, hoặc thậm chí struct khác). (dev.to, medium.com)

  • Ví dụ khai báo struct để lưu thông tin sách:

    type Book struct { ID int64 Title string Author string Pages int
    }
    

    Trong đó, Book là một struct có các trường ID, Title, Author, Pages. (medium.com)

  • Struct cho phép định nghĩa các phương thức (methods) gắn liền, ví dụ:

    func (b *Book) Summary() string { return fmt.Sprintf("%s by %s (%d pages)", b.Title, b.Author, b.Pages)
    }
    

    Nhờ đó, struct vừa chứa dữ liệu vừa chứa một phần logic liên quan. (getstream.io, medium.com)

1.2 Mục đích sử dụng

  1. Đóng gói dữ liệu: Khi cần lưu nhiều thuộc tính liên quan thành một thực thể (entity), ví dụ như User, Order, Project. (forum.golangbridge.org, medium.com)
  2. Xây dựng DTO/Model: Trong các ứng dụng web (như Go Fiber), struct thường đại diện cho các request body hoặc các bản ghi trong database. (getstream.io, youtube.com)
  3. Thực hiện cơ chế ghép (composition): Go dùng composition (embedding) thay vì kế thừa, struct có thể nhúng (embed) struct khác để tái sử dụng mã. (en.wikipedia.org)
  4. Phát triển nhanh, gọn: Struct rất gần với JSON/ORM mapping, dễ liên kết với thư viện như encoding/json, gorm, hoặc pgx. (dev.to, getstream.io)

2. Bản chất của interface trong Go

2.1 Định nghĩa và vai trò cơ bản

  • interface chỉ định nghĩa một tập hợp các phương thức (method signatures), không chứa trường dữ liệu. Ví dụ:

    type BookRepository interface { Create(book *Book) (*Book, error) GetByID(id int64) (*Book, error) GetAll() ([]*Book, error) Update(book *Book) (*Book, error) Delete(id int64) error
    }
    

    Đây là một interface liệt kê các phương thức bắt buộc để thao tác với dữ liệu kiểu Book. (dev.to, stackoverflow.com)

  • Bất kỳ kiểu nào (struct hoặc cả những kiểu tùy biến khác) chỉ cần đầy đủ các phương thức giống chữ ký trong interface là tự động được coi là “cài đặt” interface, mà không cần khai báo implements. (getstream.io, connectwise.com)

  • Ví dụ, nếu có struct:

    type novelRepo struct { // chứa pool kết nối DB db *sql.DB } func (r *novelRepo) Create(n *domain.Novel) error { /* ... */ }
    func (r *novelRepo) GetByID(id int64) (*domain.Novel, error) { /* ... */ }
    // Triển khai đầy đủ Create, GetByID, ListAll, Update, Delete...
    

    Thì &novelRepo{db: conn} tự động “thỏa” interface NovelRepository. (connectwise.com, getstream.io)

2.2 Mục đích sử dụng

  1. Tách biệt hành vi (behavior) khỏi cài đặt: Giao thức (interface) chỉ miêu tả những gì cần làm, còn cài đặt cụ thể (struct) quyết định làm như thế nào. (forum.golangbridge.org, stackoverflow.com)
  2. Dễ dàng thay thế và mock khi test: Khi hàm, service, handler nhận vào một BookRepository, bạn có thể truyền vào implementation thật (gọi DB thật) hoặc mock (giả lập) để kiểm thử. (getstream.io, youtube.com)
  3. Đa hình (polymorphism): Nhiều struct khác nhau (như pgBookRepo, mongoBookRepo) đều có thể cài đặt cùng một interface, cho phép luồng xử lý (flow) chung gọi một giao diện duy nhất mà không cần biết struct cụ thể. (forum.golangbridge.org, stackoverflow.com)
  4. **Ủng hộ nguyên tắc Dependency Inversion: Thay vì phụ thuộc trực tiếp vào implementation chi tiết, tầng business logic chỉ phụ thuộc vào interface trừu tượng. (getstream.io, en.wikipedia.org)

3. So sánh trực quan: struct vs interface

Tiêu chí struct interface
Bản chất Dữ liệu (fields) + phương thức (methods). Chỉ định nghĩa phương thức (method signatures).
Cách khai báo type Book struct { ... } type BookRepository interface { ... }
Chứa dữ liệu Có thể chứa nhiều trường khác loại. Không chứa trường, chỉ định nghĩa “hợp đồng” hành vi.
Triển khai Phải tự định nghĩa và gán giá trị cho từng trường. Các kiểu (struct) chỉ cần cài đặt phương thức để “thỏa” interface.
Phạm vi Thường dùng để mô hình hóa dữ liệu, DTO, ORM. Dùng để trừu tượng hóa, tách lớp business logic khỏi cài đặt DB.
Hỗ trợ DI/Test Phải viết code riêng để mock hoặc stub. Dễ dàng inject implementation hoặc mock để tách biệt test/production.
Tính đa hình Không hỗ trợ đa hình trực tiếp, trừ khi tự viết interface. Tích hợp tính đa hình: struct thỏa interface có thể được gán vào biến interface.
Tính kế thừa Không có kế thừa, chỉ có embedding (composition). Không có; interface hỗ trợ “duck typing” & “structural typing”.

— Bảng trên tổng hợp dựa trên tài liệu chính thức và các bài viết chuyên sâu về struct & interface trong Go. (forum.golangbridge.org, stackoverflow.com)


4. Chi tiết hơn về struct

4.1 Cú pháp khai báo

type Book struct { ID int64 Title string Author string Pages int Price float64
}
  • Mỗi trường (field) có tên và kiểu dữ liệu, tương tự record, struct trong C/C++. (medium.com, dev.to)

4.2 Phương thức (methods) gắn với struct

func (b *Book) IsExpensive() bool { return b.Price > 1000.0
}

4.3 Embedding và Composition

type AuthorInfo struct { Name string Country string
} type Novel struct { AuthorInfo // embedded struct Title string PublishedAt time.Time
}
  • Embedding (khai báo AuthorInfo ngay trong Novel) giúp Novel có sẵn các trường và phương thức của AuthorInfo mà không cần kế thừa. (en.wikipedia.org, dev.to)

5. Chi tiết hơn về interface

5.1 Cú pháp khai báo

type Formatter interface { Format() string
}
  • Formatter là một interface, liệt kê một phương thức duy nhất Format() string. (dev.to, connectwise.com)

5.2 Triển khai ngầm định (Implicit implementation)

type Book struct { Title string } func (b Book) Format() string { return "Book: " + b.Title
} var f Formatter = Book{Title: "Có một nơi để về"}

5.3 Interface rỗng (Empty Interface)

func PrintAny(v interface{}) { fmt.Println(v)
}
  • interface{} (empty interface) có thể nhận bất kỳ giá trị nào, tương tự any trong các ngôn ngữ khác. (en.wikipedia.org, stackoverflow.com)
  • Dùng trong tình huống khi không biết trước kiểu, ví dụ như json.Unmarshal trả map[string]interface{}. (medium.com, en.wikipedia.org)

5.4 Type assertion và Type switch

var v interface{} = 123
if i, ok := v.(int); ok { fmt.Println("v is int:", i)
} switch t := v.(type) {
case int: fmt.Println("int", t)
case string: fmt.Println("string", t)
default: fmt.Println("other")
}

6. Tại sao phải tách biệt structinterface

  1. Nguyên lý thiết kế:

    • Go hướng đến composition over inheritance: Struct tập trung vào dữ liệu, interface tập trung vào hành vi. (en.wikipedia.org, dev.to)
    • Điều này giúp code linh hoạt hơn: khi muốn mở rộng kiểu mới, chỉ cần triển khai thêm phương thức chứ không phải sửa đổi struct hay dùng thừa kế phức tạp. (getstream.io, forum.golangbridge.org)
  2. Tách concern (separation of concerns):

    • Khi viết repository (như BookRepository), bạn không quan tâm cài đặt chi tiết (Postgres, MongoDB, in-memory, v.v.), chỉ cần biết interface miêu tả hành vi (CRUD). (getstream.io, forum.golangbridge.org)
    • Business logic (service, handler) chỉ phụ thuộc vào interface, giảm coupling giữa tầng xử lý và tầng dữ liệu (stackoverflow.com, youtube.com)
  3. Dễ test:

    • Trong unit test, bạn dễ dàng tạo mock struct cài đặt interface để kiểm thử service mà không cần tương tác thật với cơ sở dữ liệu. (getstream.io, connectwise.com)
  4. Thống nhất cách triển khai:

    • Struct chỉ giữ giá trị, interface chỉ liệt kê phương thức. Khi muốn thay đổi cài đặt, chỉ cần đổi struct (hoặc thêm struct mới), không cần thay đổi interface. (forum.golangbridge.org, stackoverflow.com)

7. Ví dụ đầy đủ: so sánh BookRepository với struct triển khai

7.1 Interface BookRepository

// internal/domain/book_repository.go
package domain type Book struct { ID int64 Title string Author string Pages int
} type BookRepository interface { Create(book *Book) (*Book, error) GetByID(id int64) (*Book, error) GetAll() ([]*Book, error) Update(book *Book) (*Book, error) Delete(id int64) error
}

7.2 Cài đặt với pgxpool (struct)

// internal/repository/postgres/book_repo.go
package postgres import ( "context" "fmt" "github.com/jackc/pgx/v5/pgxpool" "github.com/your-org/project/internal/domain"
) type pgBookRepo struct { pool *pgxpool.Pool
} func NewBookRepo(pool *pgxpool.Pool) *pgBookRepo { return &pgBookRepo{pool: pool}
} func (r *pgBookRepo) Create(ctx context.Context, book *domain.Book) (*domain.Book, error) { query := `INSERT INTO books (title, author, pages) VALUES ($1, $2, $3) RETURNING id` err := r.pool.QueryRow(ctx, query, book.Title, book.Author, book.Pages).Scan(&book.ID) if err != nil { return nil, fmt.Errorf("pgBookRepo.Create: %w", err) } return book, nil
} func (r *pgBookRepo) GetByID(ctx context.Context, id int64) (*domain.Book, error) { query := `SELECT id, title, author, pages FROM books WHERE id = $1` row := r.pool.QueryRow(ctx, query, id) b := &domain.Book{} if err := row.Scan(&b.ID, &b.Title, &b.Author, &b.Pages); err != nil { if err.Error() == "no rows in result set" { return nil, domain.ErrNotFound } return nil, fmt.Errorf("pgBookRepo.GetByID: %w", err) } return b, nil
} func (r *pgBookRepo) GetAll(ctx context.Context) ([]*domain.Book, error) { query := `SELECT id, title, author, pages FROM books ORDER BY id DESC` rows, err := r.pool.Query(ctx, query) if err != nil { return nil, fmt.Errorf("pgBookRepo.GetAll: %w", err) } defer rows.Close() var books []*domain.Book for rows.Next() { b := &domain.Book{} if err := rows.Scan(&b.ID, &b.Title, &b.Author, &b.Pages); err != nil { return nil, fmt.Errorf("pgBookRepo.GetAll scan: %w", err) } books = append(books, b) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("pgBookRepo.GetAll rows error: %w", err) } return books, nil
} func (r *pgBookRepo) Update(ctx context.Context, book *domain.Book) (*domain.Book, error) { query := `UPDATE books SET title=$1, author=$2, pages=$3 WHERE id=$4` cmd, err := r.pool.Exec(ctx, query, book.Title, book.Author, book.Pages, book.ID) if err != nil { return nil, fmt.Errorf("pgBookRepo.Update: %w", err) } if cmd.RowsAffected() == 0 { return nil, domain.ErrNotFound } return book, nil
} func (r *pgBookRepo) Delete(ctx context.Context, id int64) error { query := `DELETE FROM books WHERE id = $1` cmd, err := r.pool.Exec(ctx, query, id) if err != nil { return fmt.Errorf("pgBookRepo.Delete: %w", err) } if cmd.RowsAffected() == 0 { return domain.ErrNotFound } return nil
}
  • pgBookRepo là một struct chứa pool kết nối (*pgxpool.Pool) và cài đặt đầy đủ các phương thức đúng chữ ký của BookRepository. (connectwise.com, getstream.io)
  • Vì tồn tại đủ 5 phương thức (Create, GetByID, GetAll, Update, Delete), Go ngầm hiểu *pgBookRepo cài đặt BookRepository. (getstream.io, connectwise.com)

8. Khi nào nên dùng struct và khi nào nên dùng interface

8.1 Dùng struct khi:

  • Bạn cần mô hình hóa thực thể (entity) với các trường dữ liệu cụ thể, ví dụ User, Book, Order, Product. (dev.to, medium.com)
  • Bạn muốn gắn các phương thức thao tác trên dữ liệu đó trực tiếp vào struct để thuận tiện truy xuất (ví dụ CalculateTotal(), Validate()). (getstream.io, medium.com)
  • Khi thực hiện mapping JSON (đối tượng request/response) hoặc mapping với ORM (GORM, pgx) để persist dữ liệu. (dev.to, youtube.com)

8.2 Dùng interface khi:

  • Bạn cần trừu tượng hóa hành vi (behavior) mà không quan tâm tới cài đặt chi tiết. Ví dụ MilkRepository phải có các phương thức CRUD, còn cài đặt có thể dùng Postgres, MongoDB, Redis, hoặc in-memory. (forum.golangbridge.org, stackoverflow.com)
  • Khi bạn muốn tách layers trong ứng dụng, chẳng hạn service (business logic) chỉ phụ thuộc vào interface, không biết đến chi tiết struct. (getstream.io, en.wikipedia.org)
  • Cần đa hình: nhiều struct khác nhau đều có thể triển khai cùng một interface (ví dụ pgBookRepo, mongoBookRepo), cùng được gán vào biến cùng kiểu interface. (forum.golangbridge.org, stackoverflow.com)
  • Để mock dễ dàng trong unit test: bạn có thể tạo struct test cài đặt interface, dùng để simulate data mà không cần kết nối thật đến cơ sở dữ liệu. (getstream.io, youtube.com)

9. Kết luận

Việc phân biệt rõ structinterface là rất quan trọng khi thiết kế hệ thống Go, đặc biệt với các framework như Go Fiber. Tóm lại:

  • Struct: Dùng để lưu trữ dữ liệu, định nghĩa các trường (fields) và thường gắn phương thức xử lý dữ liệu (methods) ngay trong struct.
  • Interface: Dùng để miêu tả tập hợp hành vi mà bất kỳ struct nào cũng có thể thỏa, giúp tách biệt logic xử lý khỏi chi tiết cài đặt.
  • Khi cài đặt repository, handler, hoặc service, bạn nên khai báo interface để service chỉ phụ thuộc vào hành vi, rồi triển khai interface đó bằng struct tương ứng (ví dụ pgBookRepo).

Bằng cách này, bạn đạt được loose coupling (lỏng lẻo phụ thuộc), dễ test, và dễ mở rộng khi muốn thay đổi cài đặt cụ thể (ví dụ chuyển từ Postgres sang MongoDB) mà không phải chỉnh sửa tầng business logic. (getstream.io, stackoverflow.com)


Tài liệu tham khảo

  1. Structs and Interfaces in Go (DEV Community) (dev.to)
  2. Difference between structs and interfaces (Go Forum) (forum.golangbridge.org)
  3. Go Structs and Interfaces Made Simple (GetStream.io) (getstream.io)
  4. Interfaces vs struct methods (Stack Overflow) (stackoverflow.com)
  5. Struct and Interface in Go (Medium, Murat Bilal) (medium.com)
  6. Struct and Interface in Go (Medium, tim.chenbw) (medium.com)
  7. Interfaces in Go (ConnectWise Blog) (connectwise.com)
  8. All You Need to Know about Struct & Interface in Go (YouTube) (youtube.com)
  9. Go (programming language) ‒ Wikipedia (Interface System) (en.wikipedia.org)

Bình luận

Bài viết tương tự

- vừa được xem lúc

So sánh Interface và Abstract trong lập trình hướng đối tượng.

Tổng quan. Interface và Abstract class là 2 khái niệm cơ bản trong lập trình OOP.

0 0 73

- vừa được xem lúc

Abstract Class và Interface, bạn có thực sự hiểu chúng ?

Chắc hẳn mọi lập trình viên đều đã quá quen với lập trình hướng đối tượng, nhưng với Abstract Class và Interface bạn có thực sự hiểu chúng. Abstract Class là gì .

0 0 49

- vừa được xem lúc

Điều gì xảy ra nếu ta implements 2 interface có cùng 1 phương thức giống nhau

Xin chào mọi người, chắc hẳn chúng ta cũng không xa lạ gì với interface bởi vì nó được áp dụng quá nhiều trong lập trình. Nhưng đối với 1 số bạn sinh viên, intern hoặc thậm chí fresher thì có vẻ inter

0 0 33

- vừa được xem lúc

Interface & Funtion trong Typescript (p1)

Trong bài này, chúng ta cùng tìm hiểu về interface & function trong Typescript. . NodeJS: https://nodejs.org/en/.

0 0 37

- vừa được xem lúc

Dart/Flutter So sánh abstract class, interface, mixin

Khi lập trình dart và flutter chắc hẳn bạn đã gặp và sử dụng abstract class, interface(implements class) và mixin nhưng liệu bạn đã hiểu rõ sự khác nhau giữa chúng, hãy cùng mình đi so sánh để làm rõ

0 0 28

- vừa được xem lúc

So sánh Interface và Type (aka Type Alias) trong TypeScript

Trong TypeScript, cả Interface và Type Alias đều được sử dụng để định nghĩa kiểu dữ liệu, nhưng chúng có những điểm khác biệt riêng và có thể phù hợp với các trường hợp khác nhau. Bài viết này sẽ so s

0 0 13