Diving into Go Interfaces

Interfaces are one of the most challenging topics for Go beginners. I decided to thoroughly understand this topic and write this article at the same time. After reading this article, you will be able to answer the following questions:

  1. What is an interface?

  2. What is the meaning of an interface?

  3. What is an empty interface?

  4. Why is the nil interface not equal to nil?

  5. Where should I place the interface?

  6. What is the interface structure?

This article will help you better understand how interfaces work in Go and how to use them correctly in your code.

homelander

What is an interface?

Interface in the Go language, it is a special type that defines a set of method signatures, but does not contain their implementation. Interfaces allow you to describe the behavior of types, which makes your code more flexible. They add abstraction, allowing you to work with different types without worrying about specific implementations.

Interface Declaration

To declare an interface, the keyword is used typefollowed by the interface name and keyword interfacefollowed by curly braces that list the method signatures that must be implemented by types that satisfy that interface. For example:

type MyInterface interface {
	MyMethod()
}

Embedding Interfaces

Go interfaces support embedding. To do this, you need to specify the name of some other interface in the declaration of the new interface.

type I interface {
	MyInterface
}

Interface meaning

An interface value can be thought of as a tuple (pair) of a value and a concrete type:

(value, type)

Type: This is the specific data type to which the value belongs. For example, it could be a string, a number (int), a structure (struct) or any other type. The type determines which methods are available to call on this value.

Value: This is a specific value that belongs to a specific underlying type. For example, this could be the string “Hello”, the number 123, or an instance of a structure such as Dog.

An example of creating a value of an interface type:

Let's create an Animal interface that requires an implementation of the Speak() method.

type Animal interface {
    Speak() string
}

Next, create a variable animal of the Animal interface type:

var animal Animal

Through design fmt.Printf("Value %v, type %T\n", animal, animal) display the value and interface type animal. We will see the following message:

Value <nil>, type <nil>

Here we see that value And type are equal to nil. This means that the animal variable does not contain any value and does not point to a specific type.

When we try to compare the interface with nil, we see that the message “animal is not nil” does not print.

if animal != nil {
    fmt.Println("animal is not nil")
}

Let's create a pointer to a new Dog object and assign dog to the animal interface.

dog := &Dog{}
animal = dog

Now that animal points to a Dog object, we can call the Speak method. This is safe because Dog implements the Speak method required by the Animal interface.

animal.Speak() // OK

Let's use the construction again fmt.Printf("Value %v, type %T\n", animal, animal) display the value and interface type. We will see the following message:

Value &{}, type *main.Dog

Type: %T indicates that animal is a pointer to Dog (*main.Dog).

Then we check to see if animal is nil:

if animal != nil {
    fmt.Println("animal is not nil")
}

Because animal points to a Dog object, the condition is satisfied and the message “animal is not nil” is printed.

Let's change the Name field of the Dog object that dog points to

dog.Name = "Шайтан"

We withdraw again via Printf type and value of the animal variable:

Value &{Шайтан}, type *main.Dog

Now the interface value of the Name field has been updated to “Shaitan” as can be seen in the output.

Calling a Method on an Interface Type

The value of an interface type is != nil when the concrete type is != nil. We can safely call a method on an interface only if the value of the interface type is != nil. Otherwise, we will panic when calling the method.

var animal Animal
animal.Speak() // паника при попытке вызвать (interface == nil)

dog := &Dog{}
animal = dog // interface != nil
animal.Speak() // OK

Description of the interface structure

iface structure

type iface struct {
	tab  *itab          // это указатель на Interface Table или itable - структуру, которая хранит некоторые метаданные о типе и список методов, используемых для удовлетворения интерфейса. 
	data unsafe.Pointer // хранимые данные (указатель на значение)
}
  • tab *itab: This is a pointer to the interface table (itable), which contains information about the type and methods needed to implement the interface. This table helps Go determine which methods are available for a given interface value and how to call them. (see description below)

  • data unsafe.Pointer: This is a pointer to the specific data or value that implements the interface. Usage unsafe.Pointer allows an interface to reference data of an arbitrary type while retaining information about how to access it through itab.

Itab structure

type itab struct {       // 40 bytes on a 64bit arch
	inter *interfacetype // тип интерфейса
	_type *_type         // все, что мы знаем про тип из которого образован элемент интерфейса
	hash  uint32         // copy of _type.hash. Used for type switches.
	_     [4]byte
	fun   [1]uintptr     // методы, которые должна описывать структура, чтобы релизовывать интерфейс
}
  • inter *interfacetype: Interface metadata.

  • _type *_type: A pointer to information about the specific type that implements the interface. This allows Go to know how to handle the data that implements the interface.

  • hash uint32: A hash of a type, which is used to optimize operations on interfaces such as type switches.

  • fun [1]uintptr: An array of pointers to functions that must be implemented to satisfy the interface. This allows methods to be called dynamically on interface values. uintptr is an integer representation of a memory address, a pointer to the first element of an array that contains pointers to methods. Array size [1]to store a pointer to the first element of the array.

An illustration of storing interface type values ​​in an interface structure:

Let's create our custom Binary type with two methods String() string And Get() uint64.

type Binary uint64

func (i Binary) String() string {
    return strconv.Uitob64(i.Get(), 2)
}

func (i Binary) Get() uint64 {
    return uint64(i)
}

Let's create an instance of the structure Binary and assign it a value:

b := Binary(200)

The value of an interface is represented as a pair of two machine words, giving a pointer to the type information stored in the interface and a pointer to the associated data.

gointer2

The first word in the interface value points to the interface table itable. It stores information about a specific type type and a list of pointers to methods fun[0]. In our case typeBinarymethods String() string And Get() uint64.

The second word indicates the meaning data. In our case data – 200.

Through design fmt.Printf("Value %v, type %T\n", num, num) display the value and interface type. We get:

Value 11001000, type main.Binary

Interview question

With this knowledge, you will be able to answer the popular interview question:

What will the program output?

func main() {
	var ptr *struct{}
	var iface interface{}
	iface = ptr
	if iface == nil {
		println("It's nil!")
	}
} 

Empty interface

An empty interface is an interface that has no methods. To implement an interface, you need to implement all its methods. To implement an empty interface, you do not need to implement any methods. Accordingly, any type in Go implements an empty interface. In other programming languages ​​this is called any. Go has it too anythis is an alias (user-defined type) to an empty interface.

When we create an empty interface variable, we can later assign it any type.

var emptyInterface interface{}

emptyInterface = dog

emptyInterface = 123

emptyInterface = true

Quote about an empty interface from an article about Go proverbs

An empty interface says nothing (interface{} says nothing)

This postulate says that interfaces—“behavioral types”—must mean something. If you create an interface, it means something and serves a specific purpose. Empty interface (interface{}) does not mean anything and does not say anything.

There are situations when it needs to be used, but they are often the exception – don't use interface{} for no reason. Newbies often overuse empty interfaces, and a lot of questions on Stack Overflow are about them.

Implicit implementation of interfaces

Go uses implicit implementation of interfaces. Other programming languages ​​require the use of a keyword implements. Go uses the concept of duck typing. In order to implement an interface, a type must implement all its methods. In this case, you can implement more methods than required, but you cannot implement fewer. One type can implement several interfaces.

drake

Polymorphism

Polymorphism is a concept that allows objects of different types to be processed through a single interface. In this example, the polymorphism is that the MakeAnimalSpeak function can accept any type that implements the Animal interface and call the Speak method without knowing the specific type of the object. This allows you to write more flexible and extensible code because you can add new types that implement the Animal interface without having to change the existing code that works with that interface. With the help of interfaces we can abstract ourselves from concrete types.

Example

Both Dog and Cat structures implement the Speak method, making them compatible with the Animal interface. The Speak method returns a string describing the sound the animal makes.

The MakeAnimalSpeak function takes a parameter of type Animal. Since Dog and Cat implement the Animal interface, they can be passed to this function. Inside the function, the Speak method is called, which returns a string, and that string is printed to the screen. The MakeAnimalSpeak function takes a parameter of type Animal. Since Dog and Cat implement the Animal interface, they can be passed to this function. Inside the function, the Speak method is called, which returns a string, and that string is printed to the screen.

type Animal interface {
	Speak() string
}

type Dog struct {
	Name string
}

func (d Dog) Speak() string {
	return fmt.Sprintf("Собака %s лает", d.Name)
}

type Cat struct {
	Name string
}

func (c Cat) Speak() string {
	return fmt.Sprintf("Кошка %s мяукает", c.Name)
}

// Функция, принимающая интерфейс Animal и вызывающая метод Speak
func MakeAnimalSpeak(a Animal) {
	fmt.Println(a.Speak())
}

func main() {
	dog := Dog{Name: "Шайтан"}
	cat := Cat{Name: "Тайсон"}

	// Вызов функции MakeAnimalSpeak для каждого животного
	MakeAnimalSpeak(dog)
	MakeAnimalSpeak(cat)
}

Type Assertion

Type Assertion allows us to call only those methods that exist on that particular type. It returns two variables: the value of the concrete type and a flag indicating whether the value of the interface type was successfully converted to the concrete type. Once the conversion is successful, we can call methods specific to that particular type.

For each structure Dog And Cat we have added unique methods:

func (d Dog) Bark() string {
	return fmt.Sprintf("%s громко лает!", d.Name)
}

func (c Cat) Purr() string {
	return fmt.Sprintf("%s мурлычет.", c.Name)
}

Let's create a function processAnimalTypeAssertionwhich takes an interface as input Animal and executes type assertion to check for a specific type to call its unique methods:

func processAnimalTypeAssertion(animal Animal) {
	if dog, ok := animal.(*Dog); ok {
		fmt.Printf("Type: %T Value: %#v\n", dog, dog)
		fmt.Println(dog.Bark())
	}
	if cat, ok := animal.(*Cat); ok {
		fmt.Printf("Type: %T Value: %#v\n", cat, cat)
		fmt.Println(cat.Purr())
	}
}

Let's call the function processAnimalTypeAssertion V main.

func main() {
	dog := &Dog{Name: "Шайтан"}
	cat := &Cat{Name: "Тайсон"}

	processAnimalTypeAssertion(dog)
	processAnimalTypeAssertion(cat)
}

When we run this code we get the following output:

Type: *main.Dog Value: &main.Dog{Name:"Шайтан"}
Шайтан громко лает!
Type: *main.Cat Value: &main.Cat{Name:"Тайсон"}
Тайсон мурлычет.

Type Switch

Type Switch provides syntactic sugar for working with Type Assertion. Thus, we can replace the processAnimalTypeAssertion function with the processAnimalTypeSwitch function:

func processAnimalTypeSwitch(animal Animal) {
	switch v := animal.(type) {
	case *Dog:
		fmt.Printf("Type: %T Value: %#v\n", v, v)
		fmt.Println(v.Bark())
	case *Cat:
		fmt.Printf("Type: %T Value: %#v\n", v, v)
		fmt.Println(v.Purr())
	default:
		fmt.Printf("Type: %T Value: %#v\n", v, v)
	}
}
game3

nil interfaces in Go

Go is very Interesting behave nil-interfaces. This is one of 50 pitfallswhich are not obvious when learning a language.

We can create an empty interface variable, and when compared with nilwe get truewhich means interface == nil. Next we can create a pointer to the structure and compare it again with nilwe get true. Everything is logical.

Now let's assign a pointer to the structure to the interface variable. Now we get false. Why is this happening? After assigning a pointer to a structure to an interface, a specific type is written in the interface, which means that the value of the interface is no longer equal to nil. Therefore, when comparing we get false.

package main

import "fmt"

type Animal interface {
	Speak() string
}

type Dog struct {
	Name string
}

func main() {
	var i interface{}
	// type == nil, value == nil
	// поэтому i == nil
	fmt.Println(i == nil) // true

	var d *Dog
	fmt.Println(d == nil) // true

	i = d
	// type == *Dog, value == nil
	// поскольку type != nil, то i != nil
	fmt.Println(i == nil) // false
}

Where is the best place to place the interface?

This section is developed based on video clip of Nikolai Tuzovin which he discussed this topic in detail and clearly.

Small spoiler: It is better to place interfaces where they are used.

Recommendations for using interfaces:

  1. Interfaces should be minimalistic.

  2. An interface should not know anything about the types that implement it.

Let's consider an example of a certain service. In this service we are interested in two layers: storage And handlers. In layer storage there is a package userswhich specifies methods for various databases: Postgres, Redis, MySQL etc.

Project structure

In order not to depend on the type of implementation, we decided to describe a general interface called Storage. The common interface contains all the methods that are necessary to interact with all databases.

package users

type User struct {
    ID   int
    Name string
    Age  int
}

type Storage interface {
    Users() ([]User, error)
    UsersByAge(age int) ([]User, error)
    User(id int) (User, error)
    Create(user User) error
    Update(user User) error
    Delete(id int) error
    // другие методы...
}

It seems that we are following rule #2 – an interface should not know anything about the types that implement it, but this is not entirely true.

We use this interface in a layer handlers. Specifically, in it we have the function Newwhich accepts an interface Storagewhich has many methods.

package userinfo

import (
	"context"
	"fmt"
	"service/handlers"
	"service/storage/users"
)

func New(userRepo users.Storage) handlers.Handler {
	return func(ctx context.Context) error {
		// Получение UID из запроса
		uid := 1
		user, err := userRepo.User(uid)
		if err != nil {
			// Обработка ошибки
			return fmt.Errorf("failed to get user: %w", err)
		}
		fmt.Printf("User: %+v\n", user)
		return nil
	}
}

In order not to drag along a huge interface with a bunch of methods, we can describe the interface at the point of use, right in this handler. In this handler we use one single method User(). This means that we can create an interface here that will have the required method. Let's create an interface UserProviderwhich will have a method User().

package userinfo

import (
	"context"
	"fmt"
	"service/handlers"
	"service/storage/users"
)

type UserProvider interface {
    User(int) (users.User, error)
}

func New(userProvider UserProvider) handlers.Handler {
	return func(ctx context.Context) error {
		// Получение UID из запроса
		uid := 1
		user, err := userProvider.User(uid)
		if err != nil {
			// Обработка ошибки
			return fmt.Errorf("failed to get user: %w", err)
		}
		fmt.Printf("User: %+v\n", user)
		return nil
	}
}

What does this give us?

  • Minimalistic interface: There is no hint of any database in our method. Method User() it just somehow returns the user.

  • Reduced connectivity: Plastic bag handlers does not depend on the package at all storage. The connectivity of system components should be as small as possible.

  • Code clarity: We have made the expectations and needs of the different parts of the system clear. When reading the code in the package handlerswe see what interface the function expects, and this interface is described in the same package. When using a large interface Storagewe see that it has many methods, and it is not immediately clear what they are for. Also, to read the interface description, you need to go to another package.

  • System flexibility: Let's say we want to pass instead of entity Postgres essence Redis. To conform to a given interface, we will have to implement all of its methods, even if they are not used.

  • Testing: When writing unit tests to test the logic of a function, we need to isolate ourselves from any database. Mocks allow us to achieve this. Since the interface is described in the package handlersthen we can generate a mock in the same package.

Disadvantages of the approach

  • Duplication of interface descriptions across all parts of the service. If we want to change method signatures, we will have to do this in all parts of the system. If we had one common interface, it would only be enough to change it in one place.

  • Beginners from other languages ​​do not always understand this approach, which is associated with duck typing and implicit implementation of interfaces in Go.

SOLID principles

I would also like to note that by adhering to this approach, we comply with the following SOLID principles:

  • Interface separation principle (I): Software entities should not depend on methods that they do not use.
    Dividing one large interface into several small ones.

  • Dependency Inversion Principle (D): Modules at higher levels should not depend on modules at lower levels. Both types of modules must depend on abstractions. Abstractions should not depend on details, details should depend on abstractions.
    Creating abstractions that allow modules to interact without directly depending on each other.

Quote about the need to split one large interface into several small ones from the article about Go postulates (Go proverbs)

The bigger the interface, the weaker the abstraction

Newbies to Go, especially those coming from Java, often believe that interfaces should be large and contain many methods. They are also often confused by the implicit satisfaction of interfaces. But the most important thing about interfaces is not this, but the culture around them, which is reflected in this postulate. The smaller the interface, the more useful it is. Pike jokes that the three most useful interfaces he's written are: io.Reader, io.Writer And interface{} — on average, three people have 0.666 methods.

Useful materials

Why interfaces are best placed at the point of use – GoLang best practices | Nikolay Tuzov

The Go Programming Language Specification

Effective Go – interfaces

Go Data Structures: Interfaces

Go Proverbs

Practical SOLID in Golang

Conclusion

I transcribed this article with my own hands. wonderful video by Nikolai Tuzov. In his video, he answers in detail the question: “Where should I put the interface?” Before publication, I contacted Nikolai, and he is not against using his material.

This article is a carefully collected collection of useful material about interfaces, which is the result of meticulous research. It contains many references and links to other sources. I tried to collect as much information as possible that could be useful in preparing for an interview on the topic of interfaces.

All examples can be found in my GitHuband a detailed description is available at YouTube.

If you like how and what I write about, then I will be grateful for subscribing to my TG channel Go Alive (this way you definitely won’t miss new articles).

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *