Eine unkomplizierte Anleitung für die Go-Schnittstelle

DDD
Freigeben: 2024-09-18 18:39:14
Original
991 Leute haben es durchsucht

Als Entwickler, der von JavaScript zu Go wechselte, empfand ich das Konzept der Schnittstellen als Herausforderung, als ich anfing, Go zu lernen.
Dieser Leitfaden soll eine einfache Erklärung der Go-Schnittstellen für diejenigen bieten, die sich in ähnlichen Situationen befinden – unabhängig davon, ob Sie über einen JavaScript-Hintergrund verfügen oder neu in der Programmierung sind.

Ziel ist es, die Leistungsfähigkeit und Flexibilität von Go-Schnittstellen zu demonstrieren und gleichzeitig praktische Einblicke zu bieten. Ich hoffe, dass Sie am Ende dieses Artikels ein solides Verständnis dafür haben, wie Sie Schnittstellen in Ihren Go-Projekten effektiv nutzen können.

 

Überblick

Gehen wir einen Schritt zurück von Go und verwenden eine einfache Analogie, um zu verstehen, was eine Schnittstelle auf hoher Ebene ist.

Stellen Sie sich vor, wir leben in einer Welt ohne Uber, Lyft oder Mitfahrdienste.
Es gibt eine Person namens Tony, die verschiedene Arten von Fahrzeugen besitzt, darunter ein Auto, einen Lastwagen und ein Flugzeug. Er fragt sich: „Wie kann ich das Beste aus all diesen Fahrzeugen herausholen?“

In der Zwischenzeit ist da noch Tina, eine Verkäuferin, deren Job es erfordert, häufig zu reisen. Sie fährt nicht gerne und hat keinen Führerschein, deshalb nimmt sie normalerweise Taxis. Mit zunehmender Beförderung wird ihr Terminkalender jedoch voller und dynamischer, und Taxis können ihren Bedürfnissen manchmal nicht gerecht werden.

Schauen wir uns Tinas Zeitplan von Montag bis Mittwoch an:

  • Montag: Sie muss das Büro von Kunde A bis 14:00 Uhr erreichen und während der Fahrt wegen einer wichtigen Besprechung um 13:00 Uhr ihren Laptop benutzen.
  • Dienstag: Sie muss bis 17:00 Uhr im Büro von Kunde B sein. Der Weg zur Arbeit dauert etwa 2 Stunden, daher braucht sie ein Auto, in dem sie sich hinlegen und ein Nickerchen machen kann.
  • Mittwoch: Sie muss bis 10 Uhr morgens mit viel Gepäck am Flughafen sein und benötigt dafür ein geräumiges Fahrzeug.

Eines Tages entdeckt Tina, dass Tony mehrere verschiedene Fahrzeugtypen hat und sie je nach ihren Bedürfnissen das am besten geeignete auswählen kann.

A straightforward guide for Go interfaceIn diesem Szenario muss Tina jedes Mal, wenn sie irgendwohin will, Tony besuchen und sich seine Erklärung aller technischen Details anhören, bevor sie sich für ein geeignetes Auto entscheidet. Diese technischen Details interessieren Tina jedoch nicht und sie muss sie auch nicht kennen. Sie braucht einfach ein Auto, das ihren Anforderungen entspricht.

Hier ist ein einfaches Diagramm, das die Beziehung zwischen Tina und Tony in diesem Szenario veranschaulicht:
A straightforward guide for Go interfaceWie wir sehen können, fragt Tina Tony direkt. Mit anderen Worten: Tina ist auf Tony angewiesen, weil sie ihn direkt kontaktieren muss, wenn sie ein Auto braucht.

 

Um Tinas Leben einfacher zu machen, erstellt sie einen Vertrag mit Tony, der im Wesentlichen eine Liste von Anforderungen an das Auto darstellt. Tony wird dann anhand dieser Liste das am besten geeignete Auto auswählen.
A straightforward guide for Go interfaceIn diesem Beispiel hilft der Vertrag mit einer Anforderungsliste Tina, die Details des Autos zu abstrahieren und sich nur auf ihre Anforderungen zu konzentrieren. Tina muss lediglich die Anforderungen im Vertrag definieren und Tony wählt das für sie am besten geeignete Auto aus.

Wir können die Beziehung in diesem Szenario mit diesem Diagramm weiter veranschaulichen:
A straightforward guide for Go interfaceAnstatt Tony direkt zu fragen, kann Tina jetzt den Vertrag nutzen, um das Auto zu bekommen, das sie braucht. In diesem Fall ist sie nicht mehr von Tony abhängig; Stattdessen ist sie auf den Vertrag angewiesen. Der Hauptzweck des Vertrags besteht darin, die Details des Autos zu abstrahieren, sodass Tina die spezifischen Details des Autos nicht kennen muss. Sie muss lediglich wissen, dass das Auto ihren Anforderungen entspricht.

Anhand dieses Diagramms können wir die folgenden Merkmale identifizieren:

  • Der Vertrag wird von Tina definiert; Es liegt an ihr, zu entscheiden, welche Anforderungen sie benötigt.
  • Der Vertrag fungiert als Vermittler zwischen Tony und Tina. Sie sind nicht direkt voneinander abhängig; Stattdessen sind sie auf den Vertrag angewiesen.
    • Tina kann den gleichen Vertrag mit anderen nutzen, wenn Tony die Dienstleistung nicht mehr erbringt.
  • Möglicherweise gibt es mehrere Autos, die die gleichen Anforderungen erfüllen
    • Zum Beispiel könnten sowohl ein Tesla Model S als auch eine Mercedes-Benz S-Klasse Tinas Anforderungen erfüllen.

Ich hoffe, dass dieses Diagramm für Sie Sinn macht, denn sein Verständnis ist der Schlüssel zum Verständnis des Schnittstellenkonzepts. Ähnliche Diagramme werden in den folgenden Abschnitten erscheinen und dieses wichtige Konzept untermauern.

 

Was ist eine Schnittstelle?

Im vorherigen Beispiel ist ein Vertrag mit einer Liste von Anforderungen genau das, was eine Schnittstelle in Go ist.

  1. Ein Vertrag hilft Tina, die Details des Autos zu abstrahieren und sich nur auf ihre Anforderungen zu konzentrieren.
    • Eine Schnittstelle abstrahiert die Details einer Implementierung und konzentriert sich nur auf das Verhalten.
  2. Ein Vertrag wird durch eine Liste von Anforderungen definiert.
    • Eine Schnittstelle wird durch eine Liste von Methodensignaturen definiert.
  3. Jedes Auto, das die Anforderungen erfüllt, erfüllt den Vertrag.
    • Jeder Typ, der alle in der Schnittstelle angegebenen Methoden implementiert, soll diese Schnittstelle implementieren.
  4. Ein Vertrag ist Eigentum des Verbrauchers (in diesem Fall Tina).
    • Eine Schnittstelle gehört dem Benutzer (dem Aufrufer)
  5. Ein Vertrag fungiert als Vermittler zwischen Tina und Tony
    • Eine Schnittstelle fungiert als Vermittler zwischen dem Aufrufer und dem Implementierer
  6. Möglicherweise gibt es mehrere Autos, die die gleichen Anforderungen erfüllen
    • Es kann mehrere Implementierungen einer Schnittstelle geben

Ein Hauptmerkmal, das Go von vielen anderen Sprachen unterscheidet, ist die Verwendung einer impliziten Schnittstellenimplementierung. Das bedeutet, dass Sie nicht explizit deklarieren müssen, dass ein Typ eine Schnittstelle implementiert. Solange ein Typ alle für eine Schnittstelle erforderlichen Methoden definiert, implementiert er diese Schnittstelle automatisch.

Bei der Arbeit mit Schnittstellen in Go ist zu beachten, dass nur die Verhaltensliste und nicht die detaillierte Implementierung bereitgestellt wird. Eine Schnittstelle definiert, welche Methoden ein Typ haben soll, nicht wie sie funktionieren sollen.

 

Einfaches Beispiel

Lassen Sie uns ein einfaches Beispiel durchgehen, um zu veranschaulichen, wie Schnittstellen in Go funktionieren.

Zuerst definieren wir eine Autoschnittstelle:

type Car interface {
    Drive()
}
Nach dem Login kopieren

Diese einfache Car-Schnittstelle verfügt über eine einzige Methode, Drive(), die keine Argumente akzeptiert und nichts zurückgibt. Jeder Typ, der über eine Drive()-Methode mit genau dieser Signatur verfügt, wird als Implementierung der Car-Schnittstelle angesehen.

Jetzt erstellen wir einen Tesla-Typ, der die Car-Schnittstelle implementiert:

type Tesla struct{}

func (t Tesla) Drive() {
    println("driving a tesla")
}
Nach dem Login kopieren

Der Tesla-Typ implementiert die Car-Schnittstelle, weil er über eine Drive()-Methode mit derselben Signatur verfügt, die in der Schnittstelle definiert ist.

Um zu demonstrieren, wie wir diese Schnittstelle verwenden können, erstellen wir eine Funktion, die jedes Auto akzeptiert:

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

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

Dieser Code beweist, dass der Tesla-Typ die Car-Schnittstelle implementiert, da wir einen Tesla-Wert an die DriveCar-Funktion übergeben können, die jedes Auto akzeptiert.
Hinweis: Den vollständigen Code finden Sie in diesem Repository.

Es ist wichtig zu verstehen, dass Tesla die Car-Schnittstelle implizit implementiert. Es gibt keine explizite Deklaration wie die Typ-Tesla-Struktur, die die Car-Schnittstelle implementiert. Stattdessen erkennt Go, dass Tesla Car implementiert, einfach weil es über eine Drive()-Methode mit der richtigen Signatur verfügt.

Lassen Sie uns die Beziehung zwischen dem Tesla-Typ und der Car-Schnittstelle anhand eines Diagramms visualisieren:
A straightforward guide for Go interfaceDieses Diagramm veranschaulicht die Beziehung zwischen dem Tesla-Typ und der Car-Schnittstelle. Beachten Sie, dass die Car-Schnittstelle nichts über den Tesla-Typ weiß. Es ist egal, welcher Typ es implementiert, und es muss es auch nicht wissen.

Ich hoffe, dass dieses Beispiel dazu beiträgt, das Konzept der Schnittstellen in Go zu verdeutlichen. Machen Sie sich keine Sorgen, wenn Sie sich über die praktischen Vorteile der Verwendung einer Schnittstelle in diesem einfachen Szenario wundern. Im nächsten Abschnitt werden wir die Leistungsfähigkeit und Flexibilität von Schnittstellen in komplexeren Situationen untersuchen.

 

Anwendungsfall

In diesem Abschnitt untersuchen wir einige praktische Beispiele, um zu sehen, warum Schnittstellen nützlich sind.

Polymorphismus

Was Schnittstellen so leistungsstark macht, ist ihre Fähigkeit, Polymorphismus in Go zu erreichen.
Polymorphismus, ein Konzept der objektorientierten Programmierung, ermöglicht es uns, verschiedene Arten von Objekten gleich zu behandeln. Einfacher ausgedrückt ist Polymorphismus nur ein schickes Wort für „viele Formen haben“.
In der Go-Welt können wir uns Polymorphismus als „eine Schnittstelle, mehrere Implementierungen“ vorstellen.

Lassen Sie uns dieses Konzept anhand eines Beispiels untersuchen. Stellen Sie sich vor, wir möchten ein einfaches ORM (Object-Relational Mapping) erstellen, das mit verschiedenen Datenbanktypen arbeiten kann. Wir möchten, dass der Client problemlos Daten in die Datenbank einfügen, aktualisieren und löschen kann, ohne sich um die spezifische Abfragesyntax kümmern zu müssen.
Nehmen wir für dieses Beispiel an, dass wir vorerst nur MySQL und Postgres unterstützen und uns ausschließlich auf den Einfügevorgang konzentrieren. Letztendlich möchten wir, dass der Kunde unser ORM wie folgt verwendet:

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

 

Lassen Sie uns zunächst sehen, wie wir dies erreichen können, ohne eine Schnittstelle zu verwenden.
Wir beginnen mit der Definition von MySQL- und Postgres-Strukturen, jeweils mit einer Insert-Methode:

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
}
Nach dem Login kopieren

Als nächstes definieren wir eine ORM-Struktur mit einem Treiberfeld:

type ORM struct {
    db any
}
Nach dem Login kopieren

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}
}
Nach dem Login kopieren

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")
    }
}
Nach dem Login kopieren

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
}
Nach dem Login kopieren

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
}
Nach dem Login kopieren

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

func New(db DB) *ORM {
    return &ORM{db: db}
}
Nach dem Login kopieren

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)
}
Nach dem Login kopieren

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)
Nach dem Login kopieren

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
}
Nach dem Login kopieren

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)
}
Nach dem Login kopieren

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
}
Nach dem Login kopieren

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}
}
Nach dem Login kopieren

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
}
Nach dem Login kopieren

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

mockUploader := NewS3Uploader(&MockS3Manager{})
Nach dem Login kopieren

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)
}
Nach dem Login kopieren

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
}
Nach dem Login kopieren

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)
}
Nach dem Login kopieren

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
}
Nach dem Login kopieren

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
}
Nach dem Login kopieren

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))
}
Nach dem Login kopieren

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)
}
Nach dem Login kopieren

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"})
Nach dem Login kopieren

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.

Das obige ist der detaillierte Inhalt vonEine unkomplizierte Anleitung für die Go-Schnittstelle. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Quelle:dev.to
Erklärung dieser Website
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Beliebte Tutorials
Mehr>
Neueste Downloads
Mehr>
Web-Effekte
Quellcode der Website
Website-Materialien
Frontend-Vorlage
Über uns Haftungsausschluss Sitemap
Chinesische PHP-Website:Online-PHP-Schulung für das Gemeinwohl,Helfen Sie PHP-Lernenden, sich schnell weiterzuentwickeln!