头图

interface of the Go language. I have always wanted to write an article about interface , but I have not dared to write it. It lasted for a few years, but I still dare to summarize it.

Concrete types

struct defines the memory layout of the data. Some early suggestions to include methods in structs were abandoned. On the contrary, methods are declared outside the type like ordinary functions. Description (data) and behavior (methods) are independent and orthogonal.

On the one hand, a method is just a function with a "receiver" parameter.

type Point struct { x, y float }

func (p Point) Abs() 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)
}

Abs written as a regular function, and the function has not changed.

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

On the other hand, when a method uses the value of a type when defining its behavior, it is closely related to the attached type. The method can get the value from the corresponding type, and if there is a pointer "receiver", it can also manipulate its state.

"Type" is sometimes useful, and sometimes annoying. Because the type is an abstraction of the underlying memory layout, the code will focus on things that are not business logic, but the code needs to deal with different types of data. Interface is one of the generic solutions.

// 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) {
    ...
}

Abstract types

Go's interface is just a collection of functions and also defines behavior. There is no explicit relationship between interface and type, and the type can also meet the requirements of multiple interfaces at the same time.

type Abser interface {
    Abs() float
 }

 var a Abser
 a = Point{3, 4}
 print(a.Abs())
 a = Vector{1, 2, 3, 4}
 print(a.Abs())

Point and Vector meet the requirements of Abser, but also meet the requirements of interface{}. The difference is that interface{} does not have any behavior (method).

When & How

I know the truth, but when and how to use interface?

The answer is, when you don't need to care about implementation details?

func fn(Parameter) Result

When the function writer wants to hide the implementation details, the Result should be set to the interface; when the function writer wants to provide extension points, the Parameter should be set to the interface;

Hide implementation details

Take CancelCtx as an 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 {
    ...
}

The return value of cancelCtx is 0610952f7d3e40. Note that cancelCtx is not exported, which means that users can only use Context variables to receive the return value of newCancelCtx, so as to achieve the purpose of hiding the realization. Whether there are other methods for cancelCtx and how to implement it, the user has no perception.

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

If the above is achieved, the Save method will *os.File as the write target. But there are some problems with this implementation:

  1. This implementation excludes the option of writing data to a network location. Assuming that network storage becomes a requirement, the signature of this function must be changed to affect all its callers.
  2. This implementation is difficult to test. In order to verify its operation, the test must read the contents of the file after writing it. It must also be ensured that f is written to a temporary location and is always deleted afterwards.
  3. *os.File exposes many methods unrelated to Save, such as reading directories and checking whether the path is a symbolic link.

The method can be redefined using the interface isolation principle, and the optimized implementation is:

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

However, this method still violates the single responsibility principle, it is also responsible for reading and verifying the written content. Split this part of the responsibility away and continue to optimize it as:

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

However, under what circumstances will wc be closed. Maybe Save will call Close unconditionally, or call Close if it succeeds. None of the above is a good choice. So optimize again

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

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 expansion space, 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)
}

Summarize

Regarding interface, I like the following two maxims:

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

Instead of

Return concrete types, receive interfaces as parameter
(From the example of cancelCtx, if the type is exported CancelCtx, the concrete types returned is different from the above motto)

High-level languages give developers high-level capabilities, allowing developers not to pay attention to specific values and types, but to concentrate on processing business logic (behavior, method). Interface provides this capability. In addition to interface, other problems are handled based on similar ideas:

Don’t just check errors, handle them gracefully
Handle errors based on behavior, not based on value or type

The author of this article : cyningsun
address of this article is : https://www.cyningsun.com/08-02-2021/using-golang-interface-well.html
Copyright Statement : All articles in this blog, except for special statements, use CC BY-NC-ND 3.0 CN license agreement. Please indicate the source!


有疑说
12 声望5 粉丝