When it comes to object-oriented (OOP), many people have heard of encapsulation, inheritance, polymorphism, these characteristics, in essence, object-oriented is just a software programming ideas. But from this comes the concept of object-oriented language, of which Java is the most typical representative, is a fully object-oriented language, expressed in the language level there are class and object design.

After all, the main goal of any software engineering is to achieve reusability, flexibility and extensibility, and Go is no exception.

As an example: suppose you need to put an elephant inside a refrigerator, how many steps does it take?

  • Step 1: Open the refrigerator
  • Step 2: Put the elephant in the refrigerator
  • Step 3: Close the refrigerator

These 3 steps with the process-oriented way to achieve may be 3 functions, such as openFridge, placeElephant, closeFridge, we just need to call in turn.

But from the object-oriented thinking, the refrigerator as an object, it should have 2 functions: open, close, and the elephant as an object should have a function: walk, we only need to combine the functions of these 2 objects to complete these steps.

1. Go’s object-oriented

There is no such concept as class inside Go, only struct struct, which can have attributes, such as

1
2
3
4
type Fridge struct {
    Name   string
    Status string
}

Although a function cannot be defined inside a structure, we can define a method for the structure, by following this form.

1
2
3
4
5
6
7
func (i Fridge) Open()  {
    // open
}

func (i Fridge) Close()  {
    // close
}

In this way we consider Open and Close as methods belonging to the Fridge structure, which together can be compared to the concept of classes, class variables, and class methods in an object-oriented language. Very simple to understand, no other object-oriented languages such as static classes, static properties and other features.

2. function or method?

Many people do not distinguish between these 2 word concepts, often mixed up and called, function methods are not divided, although essentially a block of code, but in different environments or slightly different.

Strictly speaking, the method is an object-oriented language inside the concept, it must belong to an object, such as Java is a fully object-oriented language, so in Java there are only methods, no functions. Functions, on the other hand, are a very traditional concept. For example, functions are first-class citizens in C, so there are only functions in C.

Back inside Go, it should actually be distinguished that generally when we say function, we mean this kind of function that does not belong to any structure and can be called directly by package name.

1
2
3
func Open()  {
    // open
}

And the method belongs to a structure, you can not directly call, you have to New an object out, and then call the object’s method.

Many languages, such as PHP, there are both functions and methods, relatively more flexible, but it is best to distinguish, although the meaning we all understand.

3. Go’s interface

The interface mentioned here is not the API interface, but the object-oriented interface, also called interface, as mentioned above Go is not a fully object-oriented language, but still provides an interface, although Go’s interface is not quite the same as other language interfaces.

Go’s interface is called Duck Type, when you see a bird walking like a duck, swimming like a duck, and quacking like a duck, then the bird can be called a duck. In the Duck Type, the focus is on the behavior of the object, what it can do, rather than on the type to which the object belongs.

In many object-oriented languages, if you want to implement an interface you have to implement all its defined abstract methods, which is mandatory, but not Go, which doesn’t even have the keyword implement, you can’t “implement” an interface!

1
2
3
4
type Duck interface {
    Walk()
    Swim()
}

However, as long as a structure implements all the methods defined by the interface, we consider that the interface is implemented.

1
2
3
4
5
6
7
8
type Dog struct {
}

func (i Dog) Walk()  {
}

func (i Dog) Swim()  {
}

4. Why do we need interfaces?

In fact, this question has been bothering me for a long time, very often we hardly use interfaces in writing business code, most of them are method and function calls, but when we look at some underlying library source code, we find that there are interfaces everywhere.

In the end when to use the interface? This is a very worthy of thinking about the problem.

Because the interface of this design, or essentially for flexibility and scalability, when to use or depends on the specific circumstances, such as configuration file library, often need to support json, yaml, ini and other formats, and a log library needs to support console, file, api various output methods. This time it is necessary to use the interface to design a flexible structure that can be achieved very easily to extend the purpose of more types.

The excessive use of interfaces can also lead to excessive code redundancy, increased reading difficulty, disguised as an increase in follow-up maintenance costs, the actual work, the company’s developers at different levels, the most simple and straightforward code is easier for other people to take over the maintenance.

In my opinion, the most practical significance of interfaces in business development code is actually to facilitate the writing of single tests. With the implementation pattern of dependency injection, you can split the dependencies between different layers and do single tests for each layer separately, thus improving code quality.

For example, in development, a module depends on another module to achieve the function, if you do not use the interface to do isolation, it is very difficult to do the test separately.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
type ArticleService struct{}

func NewArticleService() ArticleService {
    return ArticleService{}
}

func (i *ArticleService) GetArticles() ([]byte, error) {
    articles, err := NewApi().GetArticles()
    if err != nil {
        return nil, err
    }
    return articles, err
}

In this code, ArticleService is dependent on Api to get the result, and they are completely dependent on each other, so it is difficult to test the logic of ArticleService separately.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
type Api struct{}

func NewApi() Api {
    return Api{}
}

func (i Api) GetArticles() ([]byte, error) {
    get, err := http.Get("https://www.baidu.com")
    if err != nil {
        return nil, err
    }
    defer get.Body.Close()
    all, err := ioutil.ReadAll(get.Body)
    if err != nil {
        return nil, err
    }
    return all, nil
}

If you were to retrofit the code with dependency injection plus an interface, you could write it like this.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// 定义一个接口
type ApiInterface interface {
    GetArticles() ([]byte, error)
}

type ArticleService struct {
    api ApiInterface
}

func NewArticleService(api ApiInterface) ArticleService {
    return ArticleService{api}
}

func (i *ArticleService) GetArticles() ([]byte, error) {
    articles, err := i.api.GetArticles()
    if err != nil {
        return nil, err
    }
    return articles, err
}

We define an interface which has a method, then ArticleService depends on this interface, and we inject this dependency by means of parameters in the New method.

There is not much difference when we use it, we just need to initialize the Api object first and pass it into the ArticleService as a parameter, and then call it.

1
2
3
4
5
6
7
8
func main() {
    articleService := service.NewArticleService(service.NewApi())
    res, err := articleService.GetArticles()
    if err != nil {
        panic(err)
    }
    fmt.Printf("%s\n", res)
}

But its practical significance is not only that, one is that ArticleService depends on an interface, not a concrete object, which is the so-called “interface-oriented programming, not implementation”. In addition, we can do tests for ArticleService alone, and we can Mock an Api object to achieve decoupling.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
type mockApi struct {
}

func (mockApi) GetArticles() ([]byte, error) {
    return []byte(""), nil
}

func TestGetArticles(t *testing.T) {
    service := NewArticleService(mockApi{})
    articles, err := service.GetArticles()

    if err != nil {
        t.Fatal("should be nil")
    }
    if len(articles) > 0 {
        t.Fatal("should be 0")
    }
}

This writing method can shield the impact of external dependencies on the test results, focusing on their own logic testing, here is just a simple demonstration of this usage, the actual development can use some mock library more convenient testing of various situations.