Go Common Mistakes Part 15: Common Mistakes and Best Practices in Interface Usage

foreword

This is Part 15 of the Go Common Mistakes series: Common Mistakes and Best Practices for using interfaces.

The material comes from Teiva Harsanyi , a Go evangelist and now a senior engineer at Docker .

All the source codes involved in this article are open source: Go common error source code , welcome everyone to pay attention to the official account and get the latest updates of this series in time.

Common Mistakes and Best Practices

Interface is the core function of the Go language, but in daily development, the interface is often used indiscriminately, the code is over-abstracted, or the abstraction is unreasonable, resulting in obscure code.

This article first takes you to review the important concepts of interface, and then explains the common mistakes and best practices of using interface.

Review of important concepts of interface

The interface contains several methods, and you can understand that an interface represents the common behavior of a class of groups.

A structure does not need a keyword like implement to implement an interface, as long as the structure implements all the methods in the interface.

Let's use the io standard library in the Go language to illustrate the power of the interface. The io standard library contains 2 interfaces:

  • io.Reader: means reading data from a data source
  • io.Writer: Indicates writing data to the target location, such as writing to a specified file or database
Figure 2.3 io.Reader reads from a data source and fills a byte slice, whereas io.Writer writes to a target from a byte slice.

img

There is only one Read method in the io.Reader interface:

type Reader interface {
    Read(p []byte) (n int, err error)
}

Read reads up to len§ bytes into p. It returns the number of bytes read (0 <= n <= len§) and any error encountered. Even if Read returns n < len§, it may use all of p as scratch space during the call. If some data is available but not len§ bytes, Read conventionally returns what is available instead of waiting for more.

If a structure wants to implement io.Reader, it needs to implement the Read method. This method should contain the following logic:

  • Input parameter: accepts slice whose element type is byte as the input parameter of the method.
  • Method logic: read the data in the Reader object and assign it to p. For example, the Reader object may be a strings.Reader, then calling the Read method is to assign the value of the string to p.
  • Return value: Either return the number of bytes read, or return an error.

There is only one Write method in the io.Writer interface:

type Writer interface {
    Write(p []byte) (n int, err error)
}

Write writes len§ bytes from p to the underlying data stream. It returns the number of bytes written from p (0 <= n <= len§) and any error encountered that caused the write to stop early. Write must return a non-nil error if it returns n < len§. Write must not modify the slice data, even temporarily.

If a structure wants to implement io.Writer, it needs to implement the Write method. This method should contain the following logic:

  • Input parameter: accepts slice whose element type is byte as the input parameter of the method.
  • Method logic: write the value of p to the Writer object. For example, the Writer object may be an os.File type, then calling the Write method is to write the value of p into the file.
  • Return value: either returns the number of bytes written, or returns an error.

These two functions seem very abstract, and many junior Go developers don't quite understand them. Why do they need to design such two interfaces?

Imagine such a scenario, assuming we want to implement a function, the function is to copy the content of one file to another file.

  • Method 1: This function uses 2 *os.Files as parameters to read content from one file and write to another file

    func copySourceToDest(source *io.File, dest *io.File) error {
          
          
        // ...
    }
    
  • Method 2: Use io.Reader and io.Writer as parameters. Since os.File implements io.Reader and io.Writer, os.File can also be used as a parameter of the following functions to pass to source and dest.

    func copySourceToDest(source io.Reader, dest io.Writer) error {
          
          
        // ...
    }
    

    The implementation of method 2 will be more general. source can be either a file or a string object (strings.Reader), and dest can be either a file or another database object (for example, we implement an io.Writer, Write The method is to write the data to the database).

Simplicity should be considered when designing an interface. If there are many methods defined in the interface, the abstraction of the interface will not be very good.

Quoting Go Proverbs with Rob Pike 's description of interface in Go Proverbs with Rob Pike's technical sharing at Gopherfest 2015 :

The bigger the interface, the weaker the abstraction.

Of course, we can also combine multiple interfaces into one interface, which can facilitate code writing in some scenarios.

For example, io.ReaderWriter combines the methods of io.Reader and io.Writer.

type ReadWriter interface {
    Reader
    Writer
}

when to use interface

Two common scenarios for using interfaces are introduced below.

Public behavior can be abstracted as interface

For example, the io.Reader and io.Writer introduced above are good examples. The Go standard library uses a large number of interfaces. If you are interested, you can check the source code.

Use interface to make Struct member variables private

For example, the following code example:

package main
type Halloween struct {
    
    
   Day, Month string
}
func NewHalloween() Halloween {
    
    
   return Halloween {
    
     Month: "October", Day: "31" }
}
func (o Halloween) UK(Year string) string {
    
    
   return o.Day + " " + o.Month + " " + Year
}
func (o Halloween) US(Year string) string {
    
    
   return o.Month + " " + o.Day + " " + Year
}
func main() {
    
    
   o := NewHalloween()
   s_uk := o.UK("2020")
   s_us := o.US("2020")
   println(s_uk, s_us)
}

The variable o can directly access all member variables in the Halloween structure.

Sometimes we may want to make some restrictions, and do not want the member variables in the structure to be accessed and modified at will, so we can use interface.

type Country interface {
    
    
   UK(string) string
   US(string) string
}
func NewHalloween() Country {
    
    
   o := Halloween {
    
     Month: "October", Day: "31" }
   return Country(o)
}

We define a new interface to implement all the methods of Halloween, and then NewHalloween returns this interface type.

The object obtained by externally calling NewHalloween can only use the methods defined in the Halloween structure, but cannot access the member variables of the structure.

Scenarios of messing with Interface

Interface is often used indiscriminately in Go code. When many people with C# or Java development background switch to Go, they usually abstract the interface type first, and then define the specific type.

However, this is not recommended in Go.

Don’t design with interfaces, discover them.

—Rob Pike

As Rob Pike said, don't define the interface first when you come up to do code design.

Unless it is really necessary, it is not recommended to use interface in the code from the beginning.

The best practice should be not to think about the interface first, because the excessive use of the interface will make the code obscure.

We should first write the code according to the scenario without interface, and then use the interface if we finally find that using the interface can bring additional benefits.

Precautions

Some developers may have encountered some performance problems when using interfaces for method calls.

Because when the program is running, you need to find the specific implementation type of the interface in the hash table data structure, and then call the method of this type.

But this overhead is so small that it's usually not a concern.

Summarize

Interface is a core function in the Go language, but improper use can also lead to obscure code.

Therefore, don't write the interface first when you write the code.

You should first write the code according to the scene without interface, and then use the interface if you finally find that using the interface can really bring benefits.

If using an interface doesn't make the code better, then don't use the interface, it will make the code more concise and understandable.

recommended reading

open source address

Articles and sample code are open source on GitHub: Go Language Beginner, Intermediate, and Advanced Tutorials .

Official account: advanced coding. Follow the official account to get the latest Go interview questions and technology stack.

Personal website: Jincheng's Blog .

Zhihu: Wuji .

Welfare

I have compiled a gift pack of back-end development learning materials for you, including programming language entry to advanced knowledge (Go, C++, Python), back-end development technology stack, interview questions, etc.

Follow the official account "coding advanced", send a message to backend to receive a data package, this data will be updated from time to time, and add data that I think is valuable.

Send a message " Join the group ", communicate and learn with peers, and answer questions.

References

  • https://livebook.manning.com/book/100-go-mistakes-how-to-avoid-them/chapter-2/
  • https://github.com/jincheng9/go-tutorial/tree/main/workspace/lesson18
  • https://bbs.huaweicloud.com/blogs/348512

Guess you like

Origin blog.csdn.net/perfumekristy/article/details/128058578