iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🖥️

Extending existing Go interfaces with a simple Swift-like syntax

に公開

When You Want to Add Methods to an Existing Interface in Go

You might have felt the need to add methods to an existing interface (or class). In Swift, you would use an extension to add methods to an existing class, but here I will explain how to achieve something similar in Go.

For example, suppose you have a Player class in a library and use it like this. The following is Swift code:

  var player = Player()  // Create a player object
  player.printLevel()    // Call the printLevel method of the player object

As you use it, you might realize that having a printClass method would also be convenient.

  var player = Player()
  player.printLevel()
  player.printClass()  // Displays "professional" or "amateur" (Added)

In this case, to extend it so that the printClass method can be used, in Swift, you would write the definition of the extension and the printClass method in a form similar to a class.

  extension Player {

      func printClass() {
          if level >= 20 {
              print("professional")
          } else {
              print("amateur")
          }
      }
  }

By the way, the existing Player class definition looks like this. Nothing special. Extensions can be used on normally written classes.

  class Player {
      var level = 1

      func printLevel() {
          print(level)
      }
  }

While Go does not support extensions, you can write in a way that is close to it.

  player := NewPlayer()
  player.printLevel()
  PlayerEx{player}.printClass()  // Displays "professional" or "amateur" (Added)
  PlayerEx{player}.printLevel()  // Existing methods can also be called

You might think that having a subclass PlayerEx means it is not an "extension," but please bear with me. Instead, you don't need to call a function that creates a subclass object like NewPlayerEx. You can extend objects returned from existing methods. Strictly speaking, you could say PlayerEx{ } corresponds to a function that creates a subclass object, but the disadvantage is small.

By the way, you could achieve the same thing by calling a function that doesn't belong to a class like the one below, but it becomes a confusing factor in understanding the structure of objects and concepts. By making it belong to a class, you can jump to the definition of the PlayerEx class in tools like Visual Studio Code, which is a big help in understanding the structure.

  PlayerPrintClass(player)  // Not very good

(Note) The correct terms in Go are: Class -> Struct + Receiver, Method -> Receiver.

Extending Existing Interfaces Using Embedding

Here is how to write an extension for an interface.

To add an:

  • ExtraMethod receiver (method)

to the existing cron.Schedule interface, the code is as follows:

  // ScheduleEx embeds the interface
  type ScheduleEx struct {
      cron.Schedule
  }

  func (s ScheduleEx) ExtraMethod() { // Do not pass a pointer for a struct that embeds an interface
      fmt.Printf("(ScheduleEx) ExtraMethod\n")
  }

cron.Schedule is an interface, but it can be embedded into a struct. Writing only the type name without a member variable name results in embedding. For the receiver (method) of a struct that embeds an interface, please use the struct (ScheduleEx) rather than a pointer (*ScheduleEx) for the object type. Otherwise, you will get an error when writing the calling code.

If you try to define a receiver for cron.Schedule directly without embedding, you will get an error saying that the interface is defined in a different package.

  func (s cron.Schedule) ExtraMethod() {  // Error
      fmt.Printf("(cron.Schedule) ExtraMethod\n")
  }

The code to call the added method wraps it in ScheduleEx before calling, as shown below.

  func ExtensionMain() {
      schedule, _ := cron.Parse("*/5 * * * * *") // Returns cron.Schedule
      ScheduleEx{schedule}.ExtraMethod()  // The added method
  }

Since it is embedded, you can call the existing Next method of cron.Schedule from the ScheduleEx object without writing any specific definition.

  func ExtensionMain() {
      schedule, _ := cron.Parse("*/5 * * * * *") // Returns cron.Schedule
      nextTime := schedule.Next(time.Now())
      fmt.Println(nextTime)

      nextTime = ScheduleEx{schedule}.Next(time.Now()) // Same as schedule.Next
      fmt.Println(nextTime)

      ScheduleEx{schedule}.ExtraMethod()  // The added method
  }

You cannot directly call the ExtraMethod receiver added to the extended ScheduleEx struct from a variable of the existing cron.Schedule type.

  schedule, _ := cron.Parse("*/5 * * * * *")
  schedule.ExtraMethod()  // Error

If you prepare a variable of the extended struct type, you can call the added receiver directly.

  schedule_, _ := cron.Parse("*/5 * * * * *") 
  schedule := ScheduleEx{schedule_}

  schedule.ExtraMethod()

Extending Pointers to Existing Structs Using Embedding

Here is how to write an extension for a pointer to a struct.

To add a:

  • ReplaceToHyphen receiver (method)

to the existing *regexp.Regexp type, the code is as follows:

  // RegExpEx embeds a pointer to a struct
  type RegExpEx struct {
      *regexp.Regexp
  }

  func (s RegExpEx) ReplaceToHyphen(source string) string { // Do not pass a pointer for a struct that embeds a pointer either
      return s.ReplaceAllString(source, "--")
  }

*regexp.Regexp is a pointer, but it can be embedded into a struct.

If you try to define a receiver for *regexp.Regexp directly without embedding, you will get an error saying that the struct is defined in a different package.

  func (s *regexp.Regexp) ReplaceToHyphen(source string) string {  // Error
      return s.ReplaceAllString(source, "--")
  }

The code to call the added method wraps it in RegExpEx before calling, as shown below.

  func ExtensionPointerMain() {
      re := regexp.MustCompile(`[A-Za-z]+`) // Returns *regexp.Regexp
      replaced := RegExpEx{re}.ReplaceToHyphen("123abc456def789")  // The added method
  }

Since it is embedded, you can call the existing ReplaceAllString method of *regexp.Regexp from the RegExpEx object without writing any specific definition.

  func ExtensionPointerMain() {
      re := regexp.MustCompile(`[A-Za-z]+`) // Returns *regexp.Regexp
      replaced = RegExpEx{re}.ReplaceAllString("123abc456def789", "--")
  }

You cannot directly call the ReplaceToHyphen receiver added to the extended RegExpEx struct from a variable of the existing *regexp.Regexp type.

  re := regexp.MustCompile(`[A-Za-z]+`)
  re.ReplaceToHyphen()  // Error

If you prepare a variable of the extended struct type, you can call the added receiver directly.

  re_ := regexp.MustCompile(`[A-Za-z]+`)
  re := RegExpEx{re_}

  re.ReplaceToHyphen()

Defining an Interface with Added Receivers

This is not used very often, but instead of an indirect call like ScheduleEx{schedule_}.ExtraMethod(), if you have a type that can be called directly like schedule.ExtraMethod(), you can also:

  • Assign it to an interface that has the added ExtraMethod receiver.

    type ScheduleInrerface interface {  // Interface definition
        Next(now time.Time) time.Time  // Existing receiver. Rewrite the contents of cron.Schedule.
        ExtraMethod()  // Added receiver
    }
    

However, code with interfaces can become difficult to follow, so please limit it to only when absolutely necessary. Even if you don't prepare it in advance for the future, you only need to modify a small amount of code, and no changes will occur in the code that uses it. APIs need stable specifications, but they don't necessarily need to be interfaces. They are only needed when calling back to the user side (another package) and for internal cases shown later.

If you need to call the existing Next receiver included in the existing interface, you must rewrite it as well. The reason you have to rewrite the existing Next receiver is that an error occurs if you try to embed the existing interface into the extended interface.

  type ScheduleInrerface interface {
      cron.Schedule  // Error
  }

Presumably, the language specification was designed with the philosophy that if you could define a new interface inherited from an existing interface (usually an interface used by the application), most of the components (receivers, etc.) of the new interface would be listed in IntelliSense or debuggers even if they aren't used, making it less user-friendly.

By the way, a struct that extends an interface can be assigned not only to variables of the new interface but also to variables of the existing interface.

  schedule := ScheduleEx{schedule_}
  newInterface := ScheduleInrerface(schedule)
  originalInterface := crom.Schedule(schedule)

Extending to Add Member Variables

This is also not used very often, but I will explain cases where you extend by adding member variables.

  schedule_, _ := cron.Parse("*/5 * * * * *") 
  schedule := CastToScheduleMemo(schedule_)

  schedule.NextTime_  // Added member variable
  schedule.ExtraMethod()  // Added receiver

The following is the code for the ScheduleMemo struct, which adds:

  • NextTime_ member variable (the main focus this time!)
  • NextTime receiver
  • ExtraMethod receiver

to the existing cron.Schedule interface.

  type ScheduleMemo struct {
      NextTime_ time.Time  // Added member variable. Memoized value
      cron.Schedule
  }

  func CastToScheduleMemo(cron cron.Schedule) *ScheduleMemo {
      s := &ScheduleMemo{
          Schedule: cron,
      }
      s.NextTime_ = s.NextTime()  // Call the added receiver internally and store it in the added member variable
      return s
  }

  func (s ScheduleMemo) NextTime() time.Time {
      return s.Next(time.Now())
  }

  func (s *ScheduleMemo) ExtraMethod() { // Added receiver. Since interface is not embedded, use pointer(*)
      fmt.Println("(ScheduleMemo) ExtraMethod")
  }

The data needed by many applications (attributes added through extension) is often only a part of the vast amount of data provided by a library, and often a slightly processed value from what the library provides. Adding member variables to store (memoize) such values is convenient and slightly faster. Especially in Go, debuggers cannot display the values of object properties (values returned by getters), so development efficiency would suffer without adding member variables.

Common Member Variables in Extended and Different Types

This is often used. You might have wanted to use a similar type or define a similar type in your application. If you realize this, a major code change will occur, so change it to the code shown below as soon as possible.

  // Object wrapping a cron.Schedule object
  schedule_, _ := cron.Parse("*/5 * * * * *")  // Next regular time
  schedule := CastToScheduleFacade(schedule_)
  schedule.NextTime       // Member variable used by the application
  schedule.ExtraMethod()  // Receiver used by the application

  // Object with similar meaning but no wrapping target
  schedule = &ScheduleFacade{NextTime: time.Now()}  // Right now
  schedule.NextTime       // Member variable used by the application
  schedule.ExtraMethod()  // Receiver used by the application

In this case, define a ScheduleFacade struct that has:

  • Common member variable NextTime (placed outside the target to be wrapped)
  • Member variables for each type of object (target to wrap, target to delegate)
  • Common receiver ExtraMethod

Instead of embedding the interface into the ScheduleFacade struct as explained so far, perform it on the delegation target type. Place the common member variable outside the target to be wrapped.

  // Definition of the new struct
  type ScheduleFacade struct {
      NextTime time.Time      // Common member variable used by the application
      cron     cron.Schedule  // object or nil. Target to wrap or delegate
  }

  // Convert the type to wrap it
  func CastToScheduleFacade(cron cron.Schedule) *ScheduleFacade {
      schedule := CronScheduleEx{cron}

      return &ScheduleFacade{
          NextTime: schedule.NextTime(),
          cron:     cron,
      }
  }

  func (s *ScheduleFacade) ExtraMethod() {
      fmt.Println("(ScheduleFacade) ExtraMethod")
  }

  // CronScheduleEx can be extended because it embeds an interface
  type CronScheduleEx struct {
      cron.Schedule
  }

  func (s CronScheduleEx) NextTime() time.Time {  // Added method
      return s.Next(time.Now())
  }

If you embed the cron.Schedule interface in the ScheduleFacade struct, it becomes difficult to determine which type it is (whether it wraps a cron.Schedule object or not). Also, by not embedding, you can expect the benefit of narrowing down the data to focus on (to common member variables used by the application) and reducing the number of accesses, similar to when separating DTO (Data Transfer Object) and DAO (Data Access Object).

Another way is to define an interface with common getters/setters and align each type to that interface, but code using interfaces is hard to follow, and adding memoized member variables has more benefits as mentioned before. If you must use an interface, use it internally. In the code above, you would define an interface with a NextTime receiver. By doing this, for frequently accessed data, you will access simple member variables, reducing the need to handle complex interfaces or getters/setters.

Discussion