# Глава 14. Некоторые приемы по работе с интерфейсами
## Сравнение интерфейсов
Интерфейсы сравнимы, если сравнимы их конкретные типы:
```go {.example_for_playground}
package main
import "fmt"
type bankAccountNumber struct {
firstGroup uint16
secondGroup uint16
thirdGroup uint16
fourthGroup uint16
}
func (b bankAccountNumber) number() string {
return fmt.Sprintf("%d-%d-%d-%d", b.firstGroup,
b.secondGroup, b.thirdGroup, b.fourthGroup)
}
type accountNumber interface {
number() string
}
func main() {
var baseNumber accountNumber = bankAccountNumber{1234,
5678, 4321, 8765}
var newNumber accountNumber = bankAccountNumber{1234,
5678, 4321, 8765}
fmt.Println(baseNumber == newNumber)
}
```
```
true
```
Интерфейсы также сравнимы с `nil`:
```go
var baseNumber accountNumber
if baseNumber == nil {
fmt.Println("Oops, something went wrong! There is no card")
}
```
```
Oops, something went wrong! There is no card
```
Однако стоит помнить, что интерфейс, содержащий `nil`, необязательно является `nil`:
```go {.example_for_playground}
package main
import "fmt"
type X struct{}
func main() {
for _, ifc := range []interface{}{
nil, // interface{}
(*X)(nil), // преобразование к типу *X
(func(int) error)(nil), //
(map[string]int)(nil), //
([]byte)(nil), //
} {
// выводим тип и результат сравнения
fmt.Printf("%T (%t)\n", ifc, ifc == nil)
}
}
```
```
<nil> (true)
*main.X (false)
func(int) error (false)
map[string]int (false)
[]uint8 (false)
```
Что выведет следующий код? В случае ошибки напишите `error`. {.task_text}
```go {.example_for_playground}
package main
import "fmt"
type musicTrack struct {
id int
name string
author string
albumIds []int
}
func (m musicTrack) String() string {
return fmt.Sprintf("%s: %s", m.author, m.name)
}
func main() {
var m fmt.Stringer = musicTrack{10,
"Come Together",
"The Beatles",
[]int{1, 2, 3},
}
var m2 fmt.Stringer = musicTrack{10,
"Come Together",
"The Beatles",
[]int{2, 3},
}
fmt.Println(m == m2)
}
```
```consoleoutput {.task_source #golang_chapter_0140_task_0010}
```
Вспомните, что срезы нельзя сравнивать друг с другом. {.task_hint}
```{.task_answer}
error
```
## Сортировка
На практике нам часто приходится сталкиваться с тем, чтобы отсортировать какие-либо данные. Например, при выдаче списка файлов директории мы можем отсортировать его по имени, дате или размеру файла. В Go для того, чтобы организовать сортировку данных, удобно использовать `sort.Interface`.
Функция `Sort` пакета `sort` умеет сортировать переменные типа `sort.Interface`. Чтобы тип удовлетворял `sort.Interface`, необходимо реализовать три метода:
```go
type Interface interface {
// Len возвращает количество элементов коллекции.
Len() int
// Less является компаратором.
// Less сообщает, должен ли элемент
// с индексом i оказаться
// перед элементом с индексом j
Less(i, j int) bool
// Swap меняет местами
// элементы с индексами i и j
Swap(i, j int)
}
```
В качестве простого примера можно привести сортировку среза целых чисел по возрастанию:
```go {.example_for_playground}
package main
import (
"fmt"
"sort"
)
type intSlice []int
func (p intSlice) Len() int { return len(p) }
func (p intSlice) Less(i, j int) bool { return p[i] < p[j] }
func (p intSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
func main() {
s := intSlice{100, 80, 200, 3756, 0}
fmt.Println(s)
sort.Sort(s)
fmt.Println(s)
}
```
```
[100 80 200 3756 0]
[0 80 100 200 3756]
```
Чтобы отсортировать срез в обратном порядке, необходимо поступить следующим образом:
```go
sort.Sort(sort.Reverse(s))
```
```
[3756 200 100 80 0]
```
Функция `sort.Reverse` возвращает тип `sort.Interface`, в котором функция `Less` вызывается с индексами в обратном порядке. Вот как это достигается в исходном коде:
```go
type reverse struct {
Interface
}
func (r reverse) Less(i, j int) bool {
return r.Interface.Less(j, i)
}
func Reverse(data Interface) Interface {
return &reverse{data}
}
```
Память `memory` выделяется частями `chunk` по 16 значений типа `int`. Вся выделенная память представляется в виде односвязного списка `linkedList`, в котором каждое значение представлено в виде `memory`. Каждый чанк имеет `id`. Отсортируйте выделенную память по возрастанию `id` чанков. {.task_text}
```go {.task_source #golang_chapter_0140_task_0020}
package main
import (
"fmt"
"sort"
)
type memory struct {
id int
chunk [16]int
}
type linkedNode struct {
next *linkedNode
value memory
}
type linkedList struct {
begin *linkedNode
}
// ваш код здесь
func (list *linkedList) print() {
node := list.begin
for node != nil {
fmt.Print(node.value)
fmt.Print("->")
node = node.next
}
fmt.Print("nil")
}
func exampleList() *linkedList {
mem0 := memory{2, [16]int{1, 2, 3}}
mem1 := memory{3, [16]int{0, 0, 4, 5, 6}}
mem2 := memory{1, [16]int{7, 0, 8}}
mem3 := memory{0, [16]int{9, 10}}
return &linkedList{&linkedNode{
&linkedNode{
&linkedNode{
&linkedNode{nil,
mem3}, mem2}, mem1}, mem0}}
}
func main() {
lst := exampleList()
lst.print()
fmt.Println()
sort.Sort(lst)
lst.print()
fmt.Println()
}
```
Чтобы отсортировать односвязный список, достаточно изменить адреса соответствующих указателей. Мы уже имели дело с односвязным списком в [задаче 1 главы 11](/courses/golang/chapters/golang_chapter_0110/#block-list). С указателями мы впервые столкнулись в [задаче 1 главы 1](/courses/golang/chapters/golang_chapter_0010/#block-pointer) {.task_hint}
```go {.task_answer}
package main
import (
"fmt"
"sort"
)
const errNilNode = "one of the nodes is nil"
type memory struct {
id int
chunk [16]int
}
type linkedNode struct {
next *linkedNode
value memory
}
type linkedList struct {
begin *linkedNode
}
func (list *linkedList) Len() int {
n := list.begin
counter := 0
for n != nil {
counter++
n = n.next
}
return counter
}
func (list *linkedList) Less(i, j int) bool {
iNode := list.numberNode(i)
if iNode == nil {
panic(errNilNode)
}
jNode := list.numberNode(j)
if jNode == nil {
panic(errNilNode)
}
return iNode.value.id < jNode.value.id
}
func (list *linkedList) Swap(i, j int) {
iNode := list.numberNode(i)
if iNode == nil {
panic(errNilNode)
}
jNode := list.numberNode(j)
if jNode == nil {
panic(errNilNode)
}
var iPrev *linkedNode = nil
var jPrev *linkedNode = nil
if i-1 >= 0 {
iPrev = list.numberNode(i - 1)
}
if j-1 >= 0 {
jPrev = list.numberNode(j - 1)
}
if iPrev != nil {
iPrev.next = jNode
}
if jPrev != nil {
jPrev.next = iNode
}
iNode.next, jNode.next = jNode.next, iNode.next
if i == 0 {
list.begin = jNode
}
if j == 0 {
list.begin = iNode
}
}
func (list *linkedList) numberNode(number int) *linkedNode {
if list.begin == nil || number < 0 {
return nil
}
inode := list.begin
for k := 0; k < number; k++ {
inode = inode.next
if inode == nil {
return nil
}
}
return inode
}
func (list *linkedList) print() {
node := list.begin
for node != nil {
fmt.Print(node.value)
fmt.Print("->")
node = node.next
}
fmt.Print("nil")
}
func exampleList() *linkedList {
mem0 := memory{2, [16]int{1, 2, 3}}
mem1 := memory{3, [16]int{0, 0, 4, 5, 6}}
mem2 := memory{1, [16]int{7, 0, 8}}
mem3 := memory{0, [16]int{9, 10}}
return &linkedList{&linkedNode{
&linkedNode{
&linkedNode{
&linkedNode{nil,
mem3}, mem2}, mem1}, mem0}}
}
func main() {
lst := exampleList()
lst.print()
fmt.Println()
sort.Sort(lst)
lst.print()
fmt.Println()
}
```
## Пользовательские ошибки
В некоторых случаях вместо стандартных ошибок удобнее создать свои. Например, когда мы используем значения ошибок, которые нигде не меняются. Сделать переменную типа `error` константой невозможно:
```go {.example_for_playground}
package main
import (
"errors"
"fmt"
)
func main() {
const err = errors.New("some error")
fmt.Println(err)
}
```
```
./main.go:9:14: errors.New("some error") (value of interface type error) is not constant (exit status 1)
```
Однако выход из этой ситуации есть. Тип `error` является интерфейсом, который требует реализовать единственный метод:
```go
type error interface {
Error() string
}
```
Сделаем псевдоним для типа `string`, а затем реализуем этот метод для него:
```go {.example_for_playground}
package main
type appErr string
func (e appErr) Error() string {
return string(e)
}
func main() {
const noInternetErr = appErr("No internet")
noInternetErr = appErr("Connection is lost")
}
```
```
./main.go:11:2: cannot assign to noInternetErr (neither addressable nor a map index expression) (exit status 1)
```
Теперь можем работать с ошибкой-константой. Попытка изменить константу приводит к ошибке компиляции.
Пользователь приложения может задействовать различные поисковые движки. При этом вызывается соответствующий метод, например, `yandexSearch` или `googleSearch`. Они возвращают пользовательскую ошибку `customError`. Реализуйте `customError` так, чтобы ошибка представляла собой структуру. Первым полем этой структуры должен быть `userId` типа `int` — идентификатор пользователя, вторым — сообщение об ошибке `message` типа `string`. {.task_text}
При формировании строки с ошибкой используйте следующий формат: `<id пользователя> : <сообщение об ошибке>`. Например: `3 : failed to log in`. {.task_text}
```go {.task_source #golang_chapter_0140_task_0030}
package main
import "fmt"
type appUser struct {
id int
login string
}
func (a appUser) yandexSearch() error {
// некоторая логика
// ...
return customErr{a.id, "no internet"}
}
func (a appUser) googleSearch() error {
// некоторая логика
// ...
return customErr{a.id, "failed to log in"}
}
func main() {
user := appUser{3, "buba"}
err := user.yandexSearch()
if err != nil {
fmt.Println(err)
}
err = user.googleSearch()
if err != nil {
fmt.Println(err)
}
}
```
Создайте структуру `customErr` и реализуйте метод `Error() string` для этого типа. {.task_hint}
```go {.task_answer}
package main
import "fmt"
type customErr struct {
userId int
message string
}
func (e customErr) Error() string {
return fmt.Sprintf("%d : %s", e.userId, e.message)
}
type appUser struct {
id int
login string
}
func (a appUser) yandexSearch() error {
// некоторая логика
// ...
return customErr{a.id, "no internet"}
}
func (a appUser) googleSearch() error {
// некоторая логика
// ...
return customErr{a.id, "failed to log in"}
}
func main() {
user := appUser{3, "buba"}
err := user.yandexSearch()
if err != nil {
fmt.Println(err)
}
err = user.googleSearch()
if err != nil {
fmt.Println(err)
}
}
```
## Декларация типов
Рассмотрим следующий код:
```go {.example_for_playground}
package main
import (
"fmt"
)
type appUser struct {
id int
login string
}
type yandexUser struct {
appUser
}
type googleUser struct {
appUser
}
type searcher interface {
search()
}
func (y yandexUser) String() string {
return fmt.Sprintf("%d : %s", y.id, y.login)
}
func (g googleUser) String() string {
return fmt.Sprintf("%d : %s", g.id, g.login)
}
func (y yandexUser) search() {
fmt.Printf("searching for yandex, %d : %s...",
y.id, y.login)
}
func (g googleUser) search() {
fmt.Printf("searching for google, %d : %s...",
g.id, g.login)
}
func main() {
var s fmt.Stringer
s = yandexUser{appUser{3, "buba"}}
fmt.Println(s)
s.search()
}
```
```
./main.go:43:4: s.search undefined (type fmt.Stringer has no field or method search) (exit status 1)
```
В функции `main` мы создали переменную `s` типа `fmt.Stringer` и присвоили ей значение типа `yandexUser`. Тип `yandexUser` удовлетворяет интерфейсу `fmt.Stringer`, поскольку реализует метод `String() string`. Мы можем вывести содержимое этой переменной в желаемом формате на экран.
Однако, что если теперь мы захотим выполнить метод `search()` для переменной `s`? Фактически в ней хранится переменная типа `yandexUser`, однако компилятор не даст нам выполнить метод. Ведь мы объявили переменную `s` с типом `fmt.Stringer`. На помощь приходит **декларация типов**:
```go
func main() {
var s fmt.Stringer
s = yandexUser{appUser{3, "buba"}}
fmt.Println(s)
s.(yandexUser).search() // декларация типа
}
```
```
3 : buba
searching for yandex, 3 : buba...
```
Декларация типа позволяет сказать компилятору, что мы хотим работать с переменной `s` как с переменной типа `yandexUser`. Компилятор проверяет, что тип переменной действительно — `yandexUser`. После этого мы можем вызвать метод `search`. В случае, если декларируемый тип не совпадает с реальным, возникает паника:
```go
func main() {
var s fmt.Stringer
s = yandexUser{appUser{3, "buba"}}
fmt.Println(s)
s.(googleUser).search() // декларация типа
}
```
```
panic: interface conversion: fmt.Stringer is main.yandexUser, not main.googleUser
```
Мы не всегда знаем наверняка, какого типа данная переменная. Чтобы избежать паники, используют следующий вид декларации типа:
```go
func main() {
var s fmt.Stringer
s = yandexUser{appUser{3, "buba"}}
fmt.Println(s)
if y, ok := s.(yandexUser); ok {
y.search()
}
}
```
```
3 : buba
searching for yandex, 3 : buba...
```
Переменная `ok` равна `True` в случае успешной декларации и `False` — в противном случае.
## Интерфейс с нулевым указателем
Этот раздел рассказывает про одно неочевидное поведение компилятора Go. Интерфейс с нулевым указателем не нулевой. Чтобы лучше это понять, обратимся к следующему примеру.
```go {.example_for_playground}
package main
import (
"errors"
"fmt"
"io"
"strconv"
"strings"
)
type filmT struct {
id int
name string
director string
screenwriter string
}
type musicTrackT struct {
id int
name string
author string
albums []int
}
type mediaReader interface {
io.Reader
}
func (f *filmT) Read(p []byte) (n int, err error) {
values := strings.Split(string(p), ",")
if len(values) != 4 {
return 0, errors.New("invalid format")
}
id, err := strconv.Atoi(values[0])
if err != nil {
// fmt.Errorf возвращает ошибку в нужном формате
return 0, fmt.Errorf("invalid format: %s", err.Error())
}
f.id = id
// убираем все пробельные символы в начале и в конце
f.name = strings.TrimSpace(values[1])
f.director = strings.TrimSpace(values[2])
f.screenwriter = strings.TrimSpace(values[3])
return len(p), nil
}
func (m *musicTrackT) Read(p []byte) (n int, err error) {
// некоторая логика
// ...
return 0, nil
}
func AddNewMedia(m mediaReader,
media []mediaReader) []mediaReader {
if m != nil {
return append(media, m)
}
return media
}
func main() {
var film *filmT = &filmT{}
_, err := film.Read([]byte("1, Forrest Gump, Robert Zemeckis, Eric Roth"))
if err != nil {
panic(err)
}
var media []mediaReader
media = AddNewMedia(film, media)
var musicTrack *musicTrackT
media = AddNewMedia(musicTrack, media)
fmt.Println(media)
}
```
```
[0xc00010e0c0 <nil>]
```
Внутри `AddNewMedia` мы проверяем переменную `m` на `nil`. Переменная `musicTrack`, переданная в `m` в функции `main`, имеет тип указателя на `musicTrackT`. Сама переменная не является `nil`. Она указывает на интерфейс `musicTrackT`, который содержит `nil`. Поэтому проверка `m != nil` внутри функции `AddNewMedia` не срабатывает так, как мы ожидаем. В итоге получаем, что в срезе `media` лежит `nil`.
Проблема решается использованием всюду типа `mediaReader`. Перепишем код из `main`, подставив в качестве типа для `film` и `musicTrack` — `mediaReader`.
```go
func main() {
var film mediaReader = &filmT{}
_, err := film.Read([]byte("1, Forrest Gump, Robert Zemeckis, Eric Roth"))
if err != nil {
panic(err)
}
var media []mediaReader
media = AddNewMedia(film, media)
var musicTrack mediaReader
media = AddNewMedia(musicTrack, media)
fmt.Println(media)
fmt.Println(media[0])
}
```
```
[0xc00010e0c0]
&{1 Forrest Gump Robert Zemeckis Eric Roth}
```
## Резюме
1. Интерфейсы можно сравнивать друг с другом, если сравнимы те типы, которых их представляют.
2. Чтобы сортировать данные стандартными средствами, достаточно объявить для этих данных некоторый тип. После этого необходимо реализовать для этого типа три метода:
* `Len() int`
* `Less(i, j int) bool`
* `Swap(i, j int)`.
3. Чтобы сортировать данные в обратном порядке, необходимо использовать функцию `sort.Reverse`. Ее применяют к переменной, после чего вызывают функцию `Sort` от результата.
4. В Go есть возможность для создание пользовательских ошибок. Это особенно удобно для создания ошибок-констант. Это позволяет обезопасить себя от непреднамеренного изменения значения ошибки, которое по смыслу меняться не должно.
5. Декларация типов позволяет сообщить компилятору о том, с переменной какого типа мы сейчас работаем.
6. Интерфейс с нулевым указателем — это не то же самое, что нулевой интерфейс.
Следующие главы находятся в разработке
Наша группа в telegram. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!