Interface is one of the most essential features of the Go language. I have always wanted to write an article about interface, but I have never dared to do so. For a couple of years, but I dare to sum it up.

Concrete types

Structs define the memory layout of data. Some early suggestions to include methods in structs were abandoned. Instead, methods are declared outside of type just like normal functions. Description (data) and behavior (methods) are independent and orthogonal.

On the one hand, a method is just a function with a “receiver” argument.

type Point struct { x, y float }

func (p Point) Abs(a) float {
    return math.Sqrt(p.x*p.x + p.y*p.y)
}

func Abs(p Point) float {
	return math.Sqrt(p.x*p.x + p.y*p.y)
}
Copy the code

Abs is written as a regular function with no change in functionality.

When should you use methods and when should you use functions? If a method does not depend on the state of the type, it should be defined as a function.

A method, on the other hand, uses the value of a type when defining its row, closely associated with the type to which it is attached. Method can get a value from the corresponding type and, if there is a pointer to “receiver”, manipulate its state.

Sometimes “type” can be useful, sometimes it can be annoying. Because a type is an abstraction of the underlying memory layout, it allows the code to focus on non-business logic, yet the code needs to deal with different types of data. Interface is one such generic solution.

// Package sort provides primitives for sorting slices and user-defined collections.
package sort

// An implementation of Interface can be sorted by the routines in this package.
// The methods refer to elements of the underlying collection by integer index.
type Interface interface {
	// Len is the number of elements in the collection.
	Len() int

	// Less reports whether the element with index i
	// must sort before the element with index j.
	Less(i, j int) bool

	// Swap swaps the elements with indexes i and j.
	Swap(i, j int)}// Sort sorts data.
func Sort(data Interface){... }Copy the code

Abstract types

Go’s interface is simply a collection of functions that also define behavior. There is no explicit relationship between an interface and a type, and a type can satisfy the requirements of more than one interface.

type Abser interface {
    Abs() float
 }

 var a Abser
 a = Point{3.4}
 print(a.Abs())
 a = Vector{1.2.3.4}
 print(a.Abs())
Copy the code

Point and Vector satisfy the requirements of Abser and interface{}. The difference is that interface{} has no behavior (method).

When & How

I understand that, but when and how to use interface?

The answer is, when you don’t have to worry about implementation details?

func fn(Parameter) Result
Copy the code

Function writers should set Result to interface when they want to hide implementation details; Function writers should set Parameter to interface when they want to provide extension points.

Hiding implementation details

Take CancelCtx for example:

type Context interface {
	Deadline() (deadline time.Time, ok bool)
	Done() <-chan struct{}
	Err() error
	Value(key interface{}) interface{}}// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
	return cancelCtx{Context: parent}
}

type cancelCtx struct{... }Copy the code

The newCancelCtx return value is cancelCtx. Note that cancelCtx is not exported, meaning that the user can only use Context variables to receive the newCancelCtx return value, thus hiding the implementation. Users have no sense of whether there is another way to cancelCtx or how to do it.

Provide extension points

When we need to persist the document

type Document struct{... }// Save writes the contents of the Document to the file f.
func (d *Document) Save(f *os.File) error
Copy the code

If this is done, the Save method targets * os.file for writing. But there are some problems with this implementation:

  1. This implementation excludes the option of writing data to a network location. Assuming network storage becomes a requirement, the signature of this function must change, affecting all its callers.
  2. This implementation is difficult to test. To verify its operation, the test must read the contents of the file after it is written. You must also ensure that f is written to a temporary location and always deleted later.
  3. * OS.file exposes a number of methods that have nothing to do with Save, such as reading directories and checking if paths are symlinks.

This approach can be redefined using the interface isolation principle to optimize implementation as:

// Save writes the contents of d to the supplied ReadWriterCloser.
func (d *Document) Save(rwc io.ReadWriteCloser) error
Copy the code

However, this approach still violates the single responsibility principle, which is responsible for both reading and validating what is written. Split this part of responsibility and continue to optimize as follows:

// Save writes the contents of d to the supplied WriteCloser.
func (d *Document) Save(wc io.WriteCloser) error
Copy the code

However, under what circumstances will WC be closed. Perhaps Save will call Close unconditionally, or call Close in case of success, which is not a good choice. So optimize again

// WriteTo writes the contents of d to the supplied Writer.
func (d *Document) WriteTo(w io.Writer) error
Copy the code

The interface declares the behavior that the caller needs, not the behavior that the type will provide. The provider of the behavior has a high degree of extensibility, for example, the decorator pattern extends the behavior.

type LogWriter struct {
    w  io.Writer
}

func (l *LogWriter)Write(p []byte) (n int, err error) {
    fmt.Printf("write len:%v".len(p))
    return l.w.Write(r)
}
Copy the code

conclusion

I like the following two quotes about interface:

Program to an ‘interface’, not an ‘implementation’ — GoF Be conservative in what you do, Be liberal in what you accept from others — restored Principle

Rather than

Return concrete types, receive interfaces as parameter (cancelCtx) Returning concrete types is inconsistent with the above maxim)

High-level languages give developers the ability to focus on business logic (behavior, method) rather than specific values and types, and interfaces provide this ability. In addition to interface, other problems are handled based on similar ideas:

Don’t just check errors, Handle them cause-based, not value – or type-based

Author: Cyningsun Author: www.cyningsun.com/08-02-2021/… Copyright notice: All articles on this blog are licensed under CC BY-NC-ND 3.0CN unless otherwise stated. Reprint please indicate the source!