ホームページ > バックエンド開発 > Golang > Go インターフェースのわかりやすいガイド

Go インターフェースのわかりやすいガイド

DDD
リリース: 2024-09-18 18:39:14
オリジナル
1133 人が閲覧しました

JavaScript から Go に移行する開発者として、最初に Go を学習し始めたとき、インターフェイスの概念は難しいと感じました。
このガイドは、JavaScript のバックグラウンドを持つ人でも、プログラミングの初心者でも、同様の状況にある人たちに Go インターフェースをわかりやすく説明することを目的としています。

目標は、実用的な洞察を提供しながら、Go インターフェースのパワーと柔軟性を実証することです。この記事を読み終えるまでに、Go プロジェクトでインターフェイスを効果的に使用する方法をしっかりと理解していただければ幸いです。

概要

Go から一歩下がって、簡単な例えを使用して、インターフェイスの概要を理解してみましょう。

私たちが Uber、Lyft、またはライドシェアリング サービスのない世界に住んでいることを想像してみてください。
車、トラック、飛行機など、さまざまな種類の乗り物を所有しているトニーという名前の人がいます。彼は、「これらの車両を最大限に活用するにはどうすればよいでしょうか?」と考えています

一方、頻繁に出張する仕事をしている販売員のティナがいます。彼女は運転が好きではなく、運転免許も持っていないので、いつもタクシーを利用します。しかし、昇進するにつれて、彼女のスケジュールはより忙しく、よりダイナミックになり、タクシーでは彼女のニーズを満たすことができないことがあります。

月曜日から水曜日までのティナのスケジュールを見てみましょう:

  • 月曜日: 彼女は午後 2 時までにクライアント A のオフィスに到着する必要があり、午後 1 時に重要な会議があるため、移動中はラップトップを使用する必要があります。
  • 火曜日: 彼女は午後 5 時までにクライアント B のオフィスに来なければなりません。通勤には約2時間かかるので、横になって仮眠できる車が必要です。
  • 水曜日: 彼女はたくさんの荷物を持って午前 10 時までに空港に着く必要があり、広い車が必要です。

ある日、ティナは、トニーがいくつかの異なるタイプの乗り物を持っており、ニーズに応じて最適なものを選択できることを発見しました。

A straightforward guide for Go interfaceこのシナリオでは、ティナがどこかに行きたいと思うたびに、適切な車を選ぶ前にトニーを訪ね、技術的な詳細についてすべて説明を聞く必要があります。ただし、ティナはこれらの技術的な詳細には関心がなく、知る必要もありません。彼女が必要としているのは、自分の要件を満たす車だけです。

これは、このシナリオにおけるティナとトニーの関係を示す簡単な図です:
A straightforward guide for Go interfaceご覧のとおり、ティナはトニーに直接尋ねます。言い換えれば、ティナは車が必要なときはいつでもトニーに直接連絡する必要があるため、トニーに依存しています。

ティナの生活を楽にするために、彼女はトニーと契約書を作成します。これは基本的に車の要件のリストです。 Tony はこのリストに基づいて最適な車を選択します。
A straightforward guide for Go interfaceこの例では、要件のリストを含む契約により、ティナは車の詳細を抽象化し、要件だけに集中することができます。ティナが行う必要があるのは、契約で要件を定義することだけです。トニーは彼女に最適な車を選択します。

このシナリオの関係を次の図でさらに詳しく説明します。
A straightforward guide for Go interfaceティナはトニーに直接頼む代わりに、契約を利用して必要な車を手に入れることができるようになりました。この場合、彼女はもうトニーに依存していません。代わりに、彼女は契約に依存します。契約の主な目的は車の詳細を抽象化することであるため、ティナは車の具体的な詳細を知る必要はありません。彼女が知る必要があるのは、その車が彼女の要件を満たしているということだけです。

この図から、次の特性を特定できます。

  • 契約はティナによって定義されます。どのような要件が必要かを決めるのは彼女次第です。
  • この契約はトニーとティナの間の仲介者として機能します。これらは相互に直接依存していません。代わりに、契約に依存します。
    • Tony がサービスの提供を停止した場合、Tina は他のユーザーと同じ契約を使用できます。
  • 同じ要件を満たす複数の車が存在する可能性があります
    • たとえば、テスラ モデル S とメルセデス ベンツ S クラスの両方がティナの要件を満たす可能性があります。

この図を理解することがインターフェイスの概念を理解する鍵となるため、この図が理解できることを願っています。以下のセクション全体で同様の図が表示され、この重要な概念が強化されます。

インターフェースとは何ですか?

前の例では、要件のリストを含むコントラクトは Go のインターフェイスとまったく同じです。

  1. 契約により、ティナは車の詳細を抽象化し、自分の要件だけに集中することができます。
    • インターフェースは実装の詳細を抽象化し、動作のみに焦点を当てます。
  2. 契約は要件のリストによって定義されます。
    • インターフェイスはメソッド シグネチャのリストによって定義されます。
  3. 要件を満たす車はすべて、契約を履行すると言われます。
    • インターフェイスで指定されたすべてのメソッドを実装する型は、そのインターフェイスを実装するといいます。
  4. 契約は消費者 (この場合はティナ) が所有します。
    • インターフェイスは、それを使用する人 (呼び出し元) によって所有されます
  5. 契約はティナとトニーの間の仲介者として機能します
    • インターフェイスは呼び出し元と実装者の間の仲介者として機能します
  6. 同じ要件を満たす複数の車が存在する可能性があります
    • インターフェースには複数の実装が存在する可能性があります

Go を他の多くの言語と区別する重要な機能は、暗黙的なインターフェイス実装の使用です。これは、型がインターフェイスを実装することを明示的に宣言する必要がないことを意味します。型がインターフェイスに必要なすべてのメソッドを定義している限り、そのインターフェイスは自動的に実装されます。

Go でインターフェイスを操作する場合、詳細な実装ではなく、動作のリストのみが提供されることに注意することが重要です。インターフェイスは、型がどのように機能するかではなく、型が持つべきメソッドを定義します。

簡単な例

Go でインターフェイスがどのように機能するかを説明するために、簡単な例を見てみましょう。

まず、Car インターフェースを定義します。

type Car interface {
    Drive()
}
ログイン後にコピー

この単純な Car インターフェイスには、引数を取らず、何も返しない Drive() という 1 つのメソッドがあります。この正確なシグネチャを持つ Drive() メソッドを持つ型は、Car インターフェイスを実装しているとみなされます。

次に、Car インターフェイスを実装する Tesla タイプを作成しましょう。

type Tesla struct{}

func (t Tesla) Drive() {
    println("driving a tesla")
}
ログイン後にコピー

Tesla タイプは、インターフェイスで定義されているものと同じシグネチャを持つ Drive() メソッドを備えているため、Car インターフェイスを実装します。

このインターフェイスの使用方法を示すために、任意の Car を受け入れる関数を作成しましょう。

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

func main() {
    t := Tesla{}
    DriveCar(t)
}
/*
Output:
driving a tesla
*/
ログイン後にコピー

このコードは、任意の Car を受け入れる DriveCar 関数に Tesla 値を渡すことができるため、Tesla タイプが Car インターフェイスを実装していることを証明します。
注: 完全なコードはこのリポジトリにあります。

Tesla が Car インターフェイスを暗黙的に実装していることを理解することが重要です。 Type Tesla struct が Car インターフェイスを実装するような明示的な宣言はありません。代わりに、Go は、正しいシグネチャを持つ Drive() メソッドがあるという理由だけで、Tesla が Car を実装していると認識します。

Tesla タイプと Car インターフェイスの関係を図で視覚化してみましょう:
A straightforward guide for Go interfaceこの図は、Tesla タイプと Car インターフェースの関係を示しています。 Car インターフェイスは Tesla タイプについて何も知らないことに注意してください。どの型が実装しているかは気にせず、知る必要もありません。

この例が Go のインターフェースの概念を明確にするのに役立つことを願っています。この単純なシナリオでインターフェイスを使用する実際的な利点について疑問に思っていても、心配する必要はありません。次のセクションでは、より複雑な状況におけるインターフェースの能力と柔軟性について検討します。

使用事例

このセクションでは、インターフェイスがなぜ役立つのかを理解するために、いくつかの実践的な例を検討します。

ポリモーフィズム

インターフェイスを非常に強力にしているのは、Go でポリモーフィズムを実現できる機能です。
オブジェクト指向プログラミングの概念であるポリモーフィズムを使用すると、異なる種類のオブジェクトを同じ方法で扱うことができます。簡単に言えば、ポリモーフィズムは「多くの形式を持つ」ことを意味する単なる派手な言葉です。
Go の世界では、ポリモーフィズムを「1 つのインターフェイス、複数の実装」と考えることができます。

例を使ってこの概念を詳しく見てみましょう。さまざまな種類のデータベースを操作できる単純な ORM (オブジェクト リレーショナル マッピング) を構築したいと想像してください。クライアントが特定のクエリ構文を気にせずに、データベースへのデータの挿入、更新、削除を簡単にできるようにしたいと考えています。
この例では、現時点では mysql と postgres のみをサポートし、挿入操作のみに焦点を当てます。最終的には、クライアントに ORM を次のように使用してもらいたいと考えています。

conn := sql.Open("mysql", "root:root@tcp(127.0.0.1:3306)/test")
orm := myorm.New(&myorm.MySQL{Conn: conn})
orm.Insert("users", user)
ログイン後にコピー

まず、インターフェースを使用せずにこれを実現する方法を見てみましょう。
まず、MySQL と Postgres の構造体を定義します。それぞれに Insert メソッドがあります:

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
}
ログイン後にコピー

次に、ドライバー フィールドを使用して ORM 構造体を定義します。

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.

以上がGo インターフェースのわかりやすいガイドの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

ソース:dev.to
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート