Rumah  >  Artikel  >  pembangunan bahagian belakang  >  Panduan ringkas untuk antara muka Go

Panduan ringkas untuk antara muka Go

DDD
DDDasal
2024-09-18 18:39:141057semak imbas

Sebagai pembangun yang beralih daripada JavaScript ke Go, saya mendapati konsep antara muka mencabar apabila saya mula belajar Go.
Panduan ini bertujuan untuk memberikan penjelasan yang jelas tentang antara muka Go untuk mereka yang berada dalam situasi yang sama - sama ada anda berasal dari latar belakang JavaScript atau baru dalam pengaturcaraan.

Matlamatnya adalah untuk menunjukkan kuasa dan fleksibiliti antara muka Go, sambil memberikan cerapan praktikal. Menjelang akhir artikel ini, saya harap anda akan mempunyai pemahaman yang kukuh tentang cara menggunakan antara muka dengan berkesan dalam projek Go anda.

 

Gambaran keseluruhan

Mari kita mundur selangkah dari Go dan gunakan analogi mudah untuk memahami maksud antara muka pada tahap tinggi.

Bayangkan kita hidup dalam dunia tanpa Uber, Lyft atau sebarang perkhidmatan perkongsian perjalanan.
Ada orang bernama Tony yang memiliki pelbagai jenis kenderaan, termasuk kereta, trak dan kapal terbang. Dia tertanya-tanya, "Bagaimanakah saya boleh memanfaatkan semua kenderaan ini?"

Sementara itu, ada Tina, seorang jurujual yang pekerjaannya memerlukan perjalanan yang kerap. Dia tidak suka memandu dan tidak mempunyai lesen memandu, jadi dia biasanya menaiki teksi. Bagaimanapun, apabila dia dinaikkan pangkat, jadualnya menjadi lebih sibuk dan dinamik, dan teksi kadangkala gagal memenuhi keperluannya.

Jom tengok jadual Tina dari Isnin hingga Rabu:

  • Isnin: Dia perlu sampai ke pejabat Pelanggan A selewat-lewatnya pada jam 2 petang dan mesti menggunakan komputer ribanya semasa perjalanan kerana ada mesyuarat penting pada jam 1 petang.
  • Selasa: Dia perlu berada di pejabat Klien B selewat-lewatnya pada pukul 5 petang. Perjalanan ulang-alik adalah kira-kira 2 jam, jadi dia memerlukan kereta untuk berbaring dan tidur sebentar.
  • Rabu: Dia perlu sampai ke lapangan terbang selewat-lewatnya pukul 10 pagi dengan membawa banyak bagasi, memerlukan kenderaan yang luas.

Pada suatu hari, Tina mendapati Tony mempunyai beberapa jenis kenderaan yang berbeza, dan dia boleh memilih yang paling sesuai berdasarkan keperluannya.

A straightforward guide for Go interfaceDalam senario ini, setiap kali Tina ingin pergi ke suatu tempat, dia perlu melawat Tony dan mendengar penjelasannya tentang semua butiran teknikal sebelum memilih kereta yang sesuai. Walau bagaimanapun, Tina tidak mengambil berat tentang butiran teknikal ini, dan dia juga tidak perlu mengetahuinya. Dia hanya memerlukan kereta yang memenuhi keperluannya.

Berikut ialah rajah ringkas yang menggambarkan hubungan antara Tina dan Tony dalam senario ini:
A straightforward guide for Go interfaceSeperti yang kita lihat, Tina terus bertanya kepada Tony. Dalam erti kata lain, Tina bergantung pada Tony kerana dia perlu menghubunginya secara terus apabila dia memerlukan kereta.

 

Untuk menjadikan hidup Tina lebih mudah, dia membuat kontrak dengan Tony, yang pada asasnya adalah senarai keperluan untuk kereta itu. Tony kemudian akan memilih kereta yang paling sesuai berdasarkan senarai ini.
A straightforward guide for Go interfaceDalam contoh ini, kontrak dengan senarai keperluan membantu Tina mengasingkan butiran kereta dan hanya menumpukan pada keperluannya. Apa yang perlu Tina lakukan ialah menentukan keperluan dalam kontrak, dan Tony akan memilih kereta yang paling sesuai untuknya.

Kita boleh menggambarkan lebih lanjut hubungan dalam senario ini dengan rajah ini:
A straightforward guide for Go interfaceDaripada bertanya terus kepada Tony, Tina kini boleh menggunakan kontrak untuk mendapatkan kereta yang dia perlukan. Dalam kes ini, dia tidak lagi bergantung kepada Tony; sebaliknya, dia bergantung pada kontrak. Tujuan utama kontrak adalah untuk mengabstraksi butiran kereta, jadi Tina tidak perlu mengetahui butiran khusus kereta itu. Apa yang dia perlu tahu ialah kereta itu memenuhi keperluannya.

Daripada rajah ini, kita boleh mengenal pasti ciri-ciri berikut:

  • Kontrak ditakrifkan oleh Tina; terpulang kepada dia untuk menentukan keperluan yang dia perlukan.
  • Kontrak bertindak sebagai orang tengah antara Tony dan Tina. Mereka tidak bergantung secara langsung antara satu sama lain; sebaliknya, mereka bergantung kepada kontrak.
    • Tina boleh menggunakan kontrak yang sama dengan orang lain jika Tony berhenti menyediakan perkhidmatan itu.
  • Mungkin terdapat beberapa kereta yang memenuhi keperluan yang sama
    • Sebagai contoh, Tesla Model S dan Mercedes-Benz S-Class boleh memenuhi keperluan Tina.

Saya harap rajah ini masuk akal kepada anda kerana memahaminya adalah kunci untuk memahami konsep antara muka. Gambar rajah yang serupa akan muncul di seluruh bahagian berikut, mengukuhkan konsep penting ini.

 

Apakah antara muka?

Dalam contoh sebelumnya, kontrak dengan senarai keperluan adalah sama seperti antara muka dalam Go.

  1. Kontrak membantu Tina mengasingkan butiran kereta dan hanya menumpukan pada keperluannya.
    • Antara muka menghilangkan butiran pelaksanaan dan hanya memfokuskan pada gelagat.
  2. Kontrak ditakrifkan oleh senarai keperluan.
    • Antara muka ditakrifkan oleh senarai tandatangan kaedah.
  3. Mana-mana kereta yang memenuhi syarat dikatakan akan melaksanakan kontrak.
    • Sebarang jenis yang melaksanakan semua kaedah yang dinyatakan dalam antara muka dikatakan melaksanakan antara muka tersebut.
  4. Kontrak dimiliki oleh pengguna (dalam kes ini, Tina)
    • Antara muka dimiliki oleh orang yang menggunakannya (pemanggil)
  5. Kontrak bertindak sebagai orang tengah antara Tina dan Tony
    • Antara muka bertindak sebagai orang tengah antara pemanggil dan pelaksana
  6. Mungkin terdapat beberapa kereta yang memenuhi keperluan yang sama
    • Mungkin terdapat berbilang pelaksanaan antara muka

Ciri utama yang membezakan Go daripada banyak bahasa lain ialah penggunaan pelaksanaan antara muka tersirat. Ini bermakna anda tidak perlu mengisytiharkan secara eksplisit bahawa jenis melaksanakan antara muka. Selagi sesuatu jenis mentakrifkan semua kaedah yang diperlukan oleh antara muka, ia secara automatik melaksanakan antara muka itu.

Apabila bekerja dengan antara muka dalam Go, adalah penting untuk ambil perhatian bahawa ia hanya menyediakan senarai gelagat, bukan pelaksanaan terperinci. Antara muka mentakrifkan kaedah yang perlu ada pada jenis, bukan cara ia sepatutnya berfungsi.

 

Contoh Mudah

Mari kita lihat contoh mudah untuk menggambarkan cara antara muka berfungsi dalam Go.

Pertama, kami akan mentakrifkan antara muka Kereta:

type Car interface {
    Drive()
}

Antara muka Kereta ringkas ini mempunyai satu kaedah, Drive(), yang tidak mengambil sebarang hujah dan tidak mengembalikan apa-apa. Mana-mana jenis yang mempunyai kaedah Drive() dengan tandatangan tepat ini dianggap sebagai melaksanakan antara muka Kereta.

Sekarang, mari buat jenis Tesla yang melaksanakan antara muka Kereta:

type Tesla struct{}

func (t Tesla) Drive() {
    println("driving a tesla")
}

Jenis Tesla melaksanakan antara muka Kereta kerana ia mempunyai kaedah Drive() dengan tandatangan yang sama seperti yang ditakrifkan dalam antara muka.

Untuk menunjukkan cara kita boleh menggunakan antara muka ini, mari buat fungsi yang menerima mana-mana Kereta:

func DriveCar(c Car) {
    c.Drive()
}

func main() {
    t := Tesla{}
    DriveCar(t)
}
/*
Output:
driving a tesla
*/

Kod ini membuktikan bahawa jenis Tesla melaksanakan antara muka Kereta kerana kami boleh menghantar nilai Tesla kepada fungsi DriveCar, yang menerima mana-mana Kereta.
Nota: Anda boleh mendapatkan kod lengkap dalam repositori ini.

Adalah penting untuk memahami bahawa Tesla melaksanakan antara muka Kereta secara tersirat. Tiada pengisytiharan eksplisit seperti jenis Tesla struct melaksanakan antara muka Kereta. Sebaliknya, Go menyedari bahawa Tesla melaksanakan Car hanya kerana ia mempunyai kaedah Drive() dengan tandatangan yang betul.

Mari kita bayangkan hubungan antara jenis Tesla dan antara muka Kereta dengan gambar rajah:
A straightforward guide for Go interfaceRajah ini menggambarkan hubungan antara jenis Tesla dan antara muka Kereta. Perhatikan bahawa antara muka Kereta tidak tahu apa-apa tentang jenis Tesla. Ia tidak kisah jenis mana yang melaksanakannya dan ia tidak perlu tahu.

Saya harap contoh ini membantu menjelaskan konsep antara muka dalam Go. Jangan risau jika anda tertanya-tanya tentang faedah praktikal menggunakan antara muka dalam senario mudah ini. Kami akan meneroka kuasa dan fleksibiliti antara muka dalam situasi yang lebih kompleks dalam bahagian seterusnya.

 

Kes guna

Dalam bahagian ini, kami akan meneroka beberapa contoh praktikal untuk melihat sebab antara muka berguna.

Polimorfisme

Apa yang menjadikan antara muka begitu hebat ialah keupayaannya untuk mencapai polimorfisme dalam Go.
Polimorfisme, konsep dalam pengaturcaraan berorientasikan objek, membolehkan kita merawat pelbagai jenis objek dengan cara yang sama. Dalam istilah yang lebih mudah, polimorfisme hanyalah perkataan mewah untuk "mempunyai banyak bentuk".
Dalam dunia Go, kita boleh menganggap polimorfisme sebagai "satu antara muka, pelbagai pelaksanaan".

Mari kita terokai konsep ini dengan contoh. Bayangkan kita ingin membina ORM (Object-Relational Mapping) ringkas yang boleh berfungsi dengan pelbagai jenis pangkalan data. Kami mahu pelanggan dapat memasukkan, mengemas kini dan memadam data daripada pangkalan data dengan mudah tanpa perlu risau tentang sintaks pertanyaan tertentu.
Untuk contoh ini, katakan kami hanya menyokong mysql dan postgres buat masa ini, dan kami akan memberi tumpuan semata-mata pada operasi sisipan. Akhirnya, kami mahu pelanggan menggunakan ORM kami seperti ini:

conn := sql.Open("mysql", "root:root@tcp(127.0.0.1:3306)/test")
orm := myorm.New(&myorm.MySQL{Conn: conn})
orm.Insert("users", user)

 

Pertama, mari kita lihat bagaimana kita boleh mencapai ini tanpa menggunakan antara muka.
Kita akan mulakan dengan mentakrifkan struct MySQL dan Postgres, masing-masing dengan kaedah Sisipan:

type MySQL struct {
    Conn *sql.DB
}

func (m *MySQL) Insert(table string, data map[string]interface{}) error {
    // insert into mysql using mysql query
}

type Postgres struct {
    Conn *sql.DB
}

func (p *Postgres) Insert(table string, data map[string]interface{}) error {
    // insert into postgres using postgres query
}

Seterusnya, kami akan mentakrifkan struct ORM dengan medan pemacu:

type ORM struct {
    db any
}

The ORM struct will be used by the client. We use the any type for the driver field because we can't determine the specific type of the driver at compile time.

Now, let's implement the New function to initialize the ORM struct:

func New(db any) *ORM {
    return &ORM{db: db}
}

Finally, we'll implement the Insert method for the ORM struct:

func (o *ORM) Insert(table string, data map[string]interface{}) error {
    switch d := o.db.(type) {
    case MySQL:
        return d.Insert(table, data)
    case Postgres:
        return d.Insert(table, data)
    default:
        return fmt.Errorf("unsupported database driver")
    }
}

We have to use a type switch (switch d := o.db.(type)) to determine the type of the driver because the db field is of type any.

While this approach works, it has a significant drawback: if we want to support more database types, we need to add more case statements. This might not seem like a big issue initially, but as we add more database types, our code becomes harder to maintain.

 

Now, let's see how interfaces can help us solve this problem more elegantly.
First, we'll define a DB interface with an Insert method:

type DB interface {
    Insert(table string, data map[string]interface{}) error
}

Any type that has an Insert method with this exact signature automatically implements the DB interface.
Recall that our MySQL and Postgres structs both have Insert methods matching this signature, so they implicitly implement the DB interface.

Next, we can use the DB interface as the type for the db field in our ORM struct:

type ORM struct {
    db DB
}

Let's update the New function to accept a DB interface:

func New(db DB) *ORM {
    return &ORM{db: db}
}

Finally, we'll modify the Insert method to use the DB interface:

func (o *ORM) Insert(table string, data map[string]interface{}) error {
    return o.db.Insert(table, data)
}

Instead of using a switch statement to determine the database type, we can simply call the Insert method of the DB interface.

Now, clients can use the ORM struct to insert data into any supported database without worrying about the specific implementation details:

// using mysql
conn := sql.Open("mysql", "root:root@tcp(127.0.0.1:3306)/test")
orm := myorm.New(&myorm.MySQL{Conn: conn})
orm.Insert("users", user)

// using postgres
conn = sql.Open("postgres", "user=pqgotest dbname=pqgotest sslmode=disable")
orm = myORM.New(&myorm.Postgres{Conn: conn})
orm.Insert("users", user)

With the DB interface, we can easily add support for more database types without modifying the ORM struct or the Insert method. This makes our code more flexible and easier to extend.

Consider the following diagram to illustrate the relationship between the ORM, MySQL, Postgres, and DB interfaces:
A straightforward guide for Go interfaceIn this diagram, the ORM struct depends on the DB interface, and the MySQL and Postgres structs implement the DB interface. This allows the ORM struct to use the Insert method of the DB interface without knowing the specific implementation details of the MySQL or Postgres structs.

This example demonstrates the power of interfaces in Go. We can have one interface and multiple implementations, allowing us to write more adaptable and maintainable code.

Note: You can find the complete code in this repository.

 

Making testing easier

Let's consider an example where we want to implement an S3 uploader to upload files to AWS S3. Initially, we might implement it like this:

type S3Uploader struct {
    client *s3.Client
}

func NewS3Uploader(client *s3.Client) *S3Uploader {
    return &S3Uploader{client: client}
}

func (s *S3Uploader) Upload(ctx context.Context, bucketName, objectKey string, data []byte) error {
    _, err := s.client.PutObject(ctx, &s3.PutObjectInput{
        Bucket: aws.String(bucketName),
        Key:    aws.String(objectKey),
        Body:   bytes.NewReader(data),
    })

    return err
}

In this example, the client field in the S3Uploader struct is type *s3.Client, which means the S3Uploader struct is directly dependent on the s3.Client.
Let's visualize this with a diagram:
A straightforward guide for Go interfaceWhile this implementation works fine during development, it can pose challenges when we're writing unit tests. For unit testing, we typically want to avoid depending on external services like S3. Instead, we'd prefer to use a mock that simulates the behavior of the S3 client.

This is where interfaces come to the rescue.

We can define an S3Manager interface that includes a PutObject method:

type S3Manager interface {
    PutObject(ctx context.Context, params *s3.PutObjectInput, optFns ...func(*s3.Options)) (*s3.PutObjectOutput, error)
}

Note that the PutObject method has the same signature as the PutObject method in s3.Client.

Now, we can use this S3Manager interface as the type for the client field in our S3Uploader struct:

type S3Uploader struct {
    client S3Manager
}

Next, we'll modify the NewS3Uploader function to accept the S3Manager interface instead of the concrete s3.Client:

func NewS3Uploader(client S3Manager) *S3Uploader {
    return &S3Uploader{client: client}
}

With this implementation, we can pass any type that has a PutObject method to the NewS3Uploader function.

For testing purposes, we can create a mock object that implements the S3Manager interface:

type MockS3Manager struct{}

func (m *MockS3Manager) PutObject(ctx context.Context, params *s3.PutObjectInput, optFns ...func(*s3.Options)) (*s3.PutObjectOutput, error) {
    // mocking logic
    return &s3.PutObjectOutput{}, nil
}

We can then pass this MockS3Manager to the NewS3Uploader function when writing unit testing.

mockUploader := NewS3Uploader(&MockS3Manager{})

This approach allows us to test the Upload method easily without actually interacting with the S3 service.

After using the interface, our diagram looks like this:
A straightforward guide for Go interfaceIn this new structure, the S3Uploader struct depends on the S3Manager interface. Both s3.Client and MockS3Manager implement the S3Manager interface. This allows us to use s3.Client for the real S3 service and MockS3Manager for mocking during unit tests.

As you might have noticed, this is also an excellent example of polymorphism in action.

Note: You can find the complete code in this repository.

 

Decoupling

In software design, it's recommended to decouple dependencies between modules. Decoupling means making the dependencies between modules as loose as possible. It helps us develop software in a more flexible way.

To use an analogy, we can think of a middleman sitting between two modules:
A straightforward guide for Go interfaceIn this case, Module A depends on the middleman, instead of directly depending on Module B.

You might wonder, what's the benefit of doing this?
Let's look at an example.

Imagine we're building a web application that takes an ID as a parameter to get a user's name. In this application, we have two packages: handler and service.
The handler package is responsible for handling HTTP requests and responses.
The service package is responsible for retrieving the user's name from the database.

Let's first look at the code for the handler package:

package handler

type Handler struct {
    // we'll implement MySQLService later
    service service.MySQLService
}

func (h *Handler) GetUserName(w http.ResponseWriter, r *http.Request) {
    id := r.URL.Query().Get("id")

    if !isValidID(id) {
        http.Error(w, "Invalid user ID", http.StatusBadRequest)
        return
    }

    userName, err := h.service.GetUserName(id)
    if err != nil {
        http.Error(w, fmt.Sprintf("Error retrieving user name: %v", err), http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(userName)
}

This code is straightforward. The Handler struct has a service field. For now, it depends on the MySQLService struct, which we'll implement later. It uses h.service.GetUserName(id) to get the user's name. The handler package's job is to handle HTTP requests and responses, as well as validation.

Now, let's look at the service package:

package service

type MySQLService struct {
    sql *sql.DB
}

func NewMySQLService(sql *sql.DB) *MySQLService {
    return &MySQLService{sql: sql}
}

func (s *MySQLService) GetUserName(id string) (string, error) {
    // get user name from database
}

Here, the MySQLService struct has an sql field, and it retrieves the user's name from the database.

In this implementation, the Handler struct is directly dependent on the MySQLService struct:
A straightforward guide for Go interfaceThis might not seem like a big deal at first, but if we want to switch to a different database, we'd have to modify the Handler struct to remove the dependency on the MySQLService struct and create a new struct for the new database.
A straightforward guide for Go interfaceThis violates the principle of decoupling. Typically, changes in one package should not affect other packages.

 

To fix this problem, we can use an interface.
We can define a Service interface that has a GetUserName method:

type Service interface {
    GetUserName(id string) (string, error)
}

We can use this Service interface as the type of the service field in the Handler struct:

package handler

type Service interface {
    GetUserName(id string) (string, error)
}

type Handler struct {
    service Service // now it depends on the Service interface
}

func (h *Handler) GetUserName(w http.ResponseWriter, r *http.Request) {
    // same as before

    // Get the user from the service
    user, err := h.service.GetUserName(id)
    if err != nil {
        http.Error(w, "Error retrieving user: "+err.Error(), http.StatusInternalServerError)
        return
    }

    // same as before
}

In this implementation, the Handler struct is no longer dependent on the MySQLService struct. Instead, it depends on the Service interface:
A straightforward guide for Go interfaceIn this design, the Service interface acts as a middleman between Handler and MySQLService.
For Handler, it now depends on behavior, rather than a concrete implementation. It doesn't need to know the details of the Service struct, such as what database it uses. It only needs to know that the Service has a GetUserName method.

When we need to switch to a different database, we can just simply create a new struct that implements the Service interface without changing the Handler struct.
A straightforward guide for Go interface

When designing code structure, we should always depend on behavior rather than implementation.
It's better to depend on something that has the behavior you need, rather than depending on a concrete implementation.

Note: You can find the complete code in this repository.

 

Working With the Standard Library

As you gain more experience with Go, you'll find that interfaces are everywhere in the standard library.
Let's use the error interface as an example.

In Go, error is simply an interface with one method, Error() string:

type error interface {
    Error() string
}

This means that any type with an Error method matching this signature implements the error interface. We can leverage this feature to create our own custom error types.

Suppose we have a function to log error messages:

func LogError(err error) {
    log.Fatal(fmt.Errorf("received error: %w", err))
}

While this is a simple example, in practice, the LogError function might include additional logic, such as adding metadata to the error message or sending it to a remote logging service.

Now, let's define two custom error types, OrderError and PaymentDeclinedError. These have different fields, and we want to log them differently:

// OrderError represents a general error related to orders
type OrderError struct {
    OrderID string
    Message string
}

func (e OrderError) Error() string {
    return fmt.Sprintf("Order %s: %s", e.OrderID, e.Message)
}

// PaymentDeclinedError represents a payment failure
type PaymentDeclinedError struct {
    OrderID string
    Reason  string
}

func (e PaymentDeclinedError) Error() string {
    return fmt.Sprintf("Payment declined for order %s: %s", e.OrderID, e.Reason)
}

Because both OrderError and PaymentDeclinedError have an Error method with the same signature as the error interface, they both implement this interface. Consequently, we can use them as arguments to the LogError function:

LogError(OrderError{OrderID: "123", Message: "Order not found"})
LogError(PaymentDeclinedError{OrderID: "123", Reason: "Insufficient funds"})

This is another excellent example of polymorphism in Go: one interface, multiple implementations. The LogError function can work with any type that implements the error interface, allowing for flexible and extensible error handling in your Go programs.

Note: You can find the complete code in this repository.

 

Summary

In this article, we've explored the concept of interfaces in Go, starting with a simple analogy and gradually moving to more complex examples.

Key takeaways about Go interfaces:

  • They are all about abstraction
  • They are defined as a set of method signatures
  • They define behavior without specifying implementation details
  • They are implemented implicitly (no explicit declaration needed)

I hope this article has helped you gain a better understanding of interfaces in Go.

 

Reference

  • Learning Go: An Idiomatic Approach to Real-World Go Programming
  • 100 Go Mistakes and How to Avoid Them
  • Golang Interfaces Explained

 

As I'm not an experienced Go developer, I welcome any feedback. If you've noticed any mistakes or have suggestions for improvement, please leave a comment. Your feedback is greatly appreciated and will help enhance this resource for others.

Atas ialah kandungan terperinci Panduan ringkas untuk antara muka Go. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Kenyataan:
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn