Эффективный GO. Интерфейсы и прочие типы
Интерфейсы
Интерфейсы в Go позволяют обозначить поведение объекта: если что-то может сделать это, то это можно использовать здесь. Мы уже видели пару простых примеров; где пользовательские принтеры могут быть реализованы с помощью метода String
, в то время как Fprintf
могут генерировать вывод для чего угодно с помощью метода Write
. Интерфейсы только с одним или двумя методами часто встречаются в Go, и им обычно дается имя, производное от имени метода, например io.Writer
для чего-то, что реализует Write
.
Тип может реализовывать несколько интерфейсов. Например, коллекция может быть отсортирована с помощью процедур из пакета sort
если она реализует sort.Interface
, в котором описаны Len()
, Less(i, j int) bool
и Swap(i, j int)
, а также имеет собственное форматирование. В этом придуманом примере Sequence
удовлетворяются оба критерия.
type Sequence []int // Методы, требуемые sort.Interface. func (s Sequence) Len() int { return len(s) } func (s Sequence) Less(i, j int) bool { return s[i] < s[j] } func (s Sequence) Swap(i, j int) { s[i], s[j] = s[j], s[i] } // Copy возвращает копию Sequence. func (s Sequence) Copy() Sequence { copy := make(Sequence, 0, len(s)) return append(copy, s...) } // Метод печати - сортирует элементы перед печатью. func (s Sequence) String() string { s = s.Copy() // Сделать копию; не перезаписывает аргумент. sort.Sort(s) str := "[" for i, elem := range s { // Цикл равен O(N²); исправим это в следующем примере. if i > 0 { str += " " } str += fmt.Sprint(elem) } return str + "]" }
Преобразования
Метод String
типа Sequence
делает работу, которую уже делает метод Sprint
но только для срезов. (Который также имеет сложность O(N²), что плохо.) Мы можем это переиспользовать (а также ускорить), если преобразуем Sequence
в плоский []int
перед вызовом Sprint
func (s Sequence) String() string { s = s.Copy() sort.Sort(s) return fmt.Sprint([]int(s)) }
Этот метод является еще одним примером метода преобразования для безопасного вызова Sprintf
из метода String
. Поскольку два типа (Sequence
и []int
) идентичны, если мы откинем имя типа, то между ними допустимо преобразование. Преобразование не создает новое значение, оно просто временно действует так, как будто уже существующее значение имеет новый тип. (Существуют и другие допустимые преобразования, такие как перобразование целого числа в число с плавающей запятой, которые создают новое значение.)
Идиомой в программах Go является преобразование типа для получения доступа к другому набору методов. Например, мы могли бы использовать существующий тип sort.IntSlice
, чтобы сократить весь пример до:
type Sequence []int // Метод печати - сортирует элементы перед печатью. func (s Sequence) String() string { s = s.Copy() sort.IntSlice(s).Sort() return fmt.Sprint([]int(s)) }
Теперь вместо того, чтобы для Sequence
реализовывать несколько интерфейсов (сортировка и печать), мы используем способность элемента к преобразованию в несколько типов ( Sequence
, sort.IntSlice
и []int
), каждый из которых выполняет определенную часть работы. Это редко используется на практике, но может быть достаточно эффективным.
Interface conversions and type assertions
Type switches are a form of conversion: they take an interface and, for each case in the switch, in a sense convert it to the type of that case. Here's a simplified version of how the code under fmt.Printf
turns a value into a string using a type switch. If it's already a string, we want the actual string value held by the interface, while if it has a String
method we want the result of calling the method.
type Stringer interface { String() string } var value interface{} // Value provided by caller. switch str := value.(type) { case string: return str case Stringer: return str.String() }
The first case finds a concrete value; the second converts the interface into another interface. It's perfectly fine to mix types this way.
What if there's only one type we care about? If we know the value holds a string
and we just want to extract it? A one-case type switch would do, but so would a type assertion. A type assertion takes an interface value and extracts from it a value of the specified explicit type. The syntax borrows from the clause opening a type switch, but with an explicit type rather than the type
keyword:
value.(typeName)
and the result is a new value with the static type typeName
. That type must either be the concrete type held by the interface, or a second interface type that the value can be converted to. To extract the string we know is in the value, we could write:
str := value.(string)
But if it turns out that the value does not contain a string, the program will crash with a run-time error. To guard against that, use the "comma, ok" idiom to test, safely, whether the value is a string:
str, ok := value.(string) if ok { fmt.Printf("string value is: %q\n", str) } else { fmt.Printf("value is not a string\n") }
If the type assertion fails, str
will still exist and be of type string, but it will have the zero value, an empty string.
As an illustration of the capability, here's an if
-else
statement that's equivalent to the type switch that opened this section.
if str, ok := value.(string); ok { return str } else if str, ok := value.(Stringer); ok { return str.String() }
Generality
If a type exists only to implement an interface and will never have exported methods beyond that interface, there is no need to export the type itself. Exporting just the interface makes it clear the value has no interesting behavior beyond what is described in the interface. It also avoids the need to repeat the documentation on every instance of a common method.
In such cases, the constructor should return an interface value rather than the implementing type. As an example, in the hash libraries both crc32.NewIEEE
and adler32.New
return the interface type hash.Hash32
. Substituting the CRC-32 algorithm for Adler-32 in a Go program requires only changing the constructor call; the rest of the code is unaffected by the change of algorithm.
A similar approach allows the streaming cipher algorithms in the various crypto
packages to be separated from the block ciphers they chain together. The Block
interface in the crypto/cipher
package specifies the behavior of a block cipher, which provides encryption of a single block of data. Then, by analogy with the bufio
package, cipher packages that implement this interface can be used to construct streaming ciphers, represented by the Stream
interface, without knowing the details of the block encryption.
The crypto/cipher
interfaces look like this:
type Block interface { BlockSize() int Encrypt(dst, src []byte) Decrypt(dst, src []byte) } type Stream interface { XORKeyStream(dst, src []byte) }
Here's the definition of the counter mode (CTR) stream, which turns a block cipher into a streaming cipher; notice that the block cipher's details are abstracted away:
// NewCTR returns a Stream that encrypts/decrypts using the given Block in // counter mode. The length of iv must be the same as the Block's block size. func NewCTR(block Block, iv []byte) Stream
NewCTR
applies not just to one specific encryption algorithm and data source but to any implementation of the Block
interface and any Stream
. Because they return interface values, replacing CTR encryption with other encryption modes is a localized change. The constructor calls must be edited, but because the surrounding code must treat the result only as a Stream
, it won't notice the difference.
Interfaces and methods
Since almost anything can have methods attached, almost anything can satisfy an interface. One illustrative example is in the http
package, which defines the Handler
interface. Any object that implements Handler
can serve HTTP requests.
type Handler interface { ServeHTTP(ResponseWriter, *Request) }
ResponseWriter
is itself an interface that provides access to the methods needed to return the response to the client. Those methods include the standard Write
method, so an http.ResponseWriter
can be used wherever an io.Writer
can be used. Request
is a struct containing a parsed representation of the request from the client.
For brevity, let's ignore POSTs and assume HTTP requests are always GETs; that simplification does not affect the way the handlers are set up. Here's a trivial implementation of a handler to count the number of times the page is visited.
// Simple counter server. type Counter struct { n int } func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) { ctr.n++ fmt.Fprintf(w, "counter = %d\n", ctr.n) }
(Keeping with our theme, note how Fprintf
can print to an http.ResponseWriter
.) In a real server, access to ctr.n
would need protection from concurrent access. See the sync
and atomic
packages for suggestions.
For reference, here's how to attach such a server to a node on the URL tree.
import "net/http" ... ctr := new(Counter) http.Handle("/counter", ctr)
But why make Counter
a struct? An integer is all that's needed. (The receiver needs to be a pointer so the increment is visible to the caller.)
// Simpler counter server. type Counter int func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) { *ctr++ fmt.Fprintf(w, "counter = %d\n", *ctr) }
What if your program has some internal state that needs to be notified that a page has been visited? Tie a channel to the web page.
// A channel that sends a notification on each visit. // (Probably want the channel to be buffered.) type Chan chan *http.Request func (ch Chan) ServeHTTP(w http.ResponseWriter, req *http.Request) { ch <- req fmt.Fprint(w, "notification sent") }
Finally, let's say we wanted to present on /args
the arguments used when invoking the server binary. It's easy to write a function to print the arguments.
func ArgServer() { fmt.Println(os.Args) }
How do we turn that into an HTTP server? We could make ArgServer
a method of some type whose value we ignore, but there's a cleaner way. Since we can define a method for any type except pointers and interfaces, we can write a method for a function. The http
package contains this code:
// The HandlerFunc type is an adapter to allow the use of // ordinary functions as HTTP handlers. If f is a function // with the appropriate signature, HandlerFunc(f) is a // Handler object that calls f. type HandlerFunc func(ResponseWriter, *Request) // ServeHTTP calls f(w, req). func (f HandlerFunc) ServeHTTP(w ResponseWriter, req *Request) { f(w, req) }
HandlerFunc
is a type with a method, ServeHTTP
, so values of that type can serve HTTP requests. Look at the implementation of the method: the receiver is a function, f
, and the method calls f
. That may seem odd but it's not that different from, say, the receiver being a channel and the method sending on the channel.
To make ArgServer
into an HTTP server, we first modify it to have the right signature.
// Argument server. func ArgServer(w http.ResponseWriter, req *http.Request) { fmt.Fprintln(w, os.Args) }
ArgServer
now has the same signature as HandlerFunc
, so it can be converted to that type to access its methods, just as we converted Sequence
to IntSlice
to access IntSlice.Sort
. The code to set it up is concise:
http.Handle("/args", http.HandlerFunc(ArgServer))
When someone visits the page /args
, the handler installed at that page has value ArgServer
and type HandlerFunc
. The HTTP server will invoke the method ServeHTTP
of that type, with ArgServer
as the receiver, which will in turn call ArgServer
(via the invocation f(w, req)
inside HandlerFunc.ServeHTTP
). The arguments will then be displayed.
In this section we have made an HTTP server from a struct, an integer, a channel, and a function, all because interfaces are just sets of methods, which can be defined for (almost) any type.
Источник: https://go.dev/doc/effective_go#interfaces_and_types