Интерфейсы используются двумя различными способами. В первом из них, примерами которого
являются io.Reader
, io.Writer
, fmt.Stringer
, sort.Interface
, http.handler
и error
, методы интерфейсов
выражают подобие конкретных типов, которые соответствуют данному интерфейсу, но скрывают детали представления и
внутренние операции этих конкретных типов. Акцент при этом делается на методах, а не на конкретных типах.
Второй стиль использует способность значения интерфейса хранить значения ряда конкретных типов и рассматривает интерфейс
как объединение
этих типов Декларации типов используются для динамического распознавания этих типов и трактовки
каждого отдельного случая по-своему. В этом стиле акцент делается на конкретных типах, соответствующих интерфейсу, а не
на методах интерфейса (если он имеет таковые), и нет никакого сокрытия информации. Мы будем говорить об интерфейсах,
используемых, используемых таким образом, как о распознаваемых объединениях
.
Если вы знакомы с объектно-ориентированным программированием, то вы можете узнать в этих двух
стилях полиморфизм подтипов
и перегрузку
, но вам не нужно запоминать эти термины. В оставшейся части главы мы
представим примеры второго стиля.
API Go для запросов к базе данных SQL, как и другие языки, позволяет отделить фиксированную часть запроса от переменных частей. Пример клиента может выглядеть следующим образом:
import "database/sql"
func listTracks(db sql.DB, artist string, minYear, maxYear int) {
result, err := db.Exec(
"SELECT * FROM tracks WHERE artist= ? AND ? <=year AND year<=?",
artist, minYear, maxYear)
//...
}
Метод Exec
заменяет каждый символ ?
в строке SQL-запроса литералом, обозначающим значение соответствующего
аргумента, который может быть логическим значением, числом, строкой или иметь значение nil. Построение запросов таким
образом помогает избежать атак SQL-инъекций, при которых злоумышленник получает контроль над запросом, используя
некорректное заключение в кавычки входных данных. В Exec
мы могли бы найти функцию наподобие показанной ниже, которая
преобразует значение каждого аргумента в его SQL-запись в виде литерала:
func sqlQuote(x interface{}) string {
if x == nil {
return "NULL"
} else if _, ok := x.(int); ok {
return fmt.Sprintf("%d", x)
} else if _, ok := x.(uint); ok {
return fmt.Sprintf("%d", x)
} else if b, ok := x.(bool); ok {
if b {
return "TRUE"
}
return "FALSE"
} else if s, ok := x.(string); ok {
return sqlQuateString(s) // (Функция не показана)
} else {
panic(fmt.Sprintf("непредвиденный тип %T: %v", x, x))
}
}
Инструкция switch
упрощает цепочку if-else
, которая выполняет последовательность проверок на равенство значений.
Аналогичная инструкция выбора типа (type switch)
упрощает цепочку if-else деклараций типов.
В простейшем виде выбор типа выглядит, как обычная инструкция switch
, в которой операндом является x.(type)
— здесь
type
представляет собой ключевое слово, — а каждая инструкция case
имеет один или несколько типов. Такая инструкция
обеспечивает множественное ветвление на основе динамического типа значения интерфейса. Случай nil
соответствует
ситуации x==nil
, а случай default
обрабатывает ситуацию, когда соответствие не найдено. Выбор типа для sqlQuote
имеет следующий вид:
switch x.(type) {
case nil: //...
case int, uint: //...
case bool: //...
case string: //...
default: //...
Как и в обычной инструкции switch
(раздел 1.8), все инструкции case
рассматриваются по порядку, и когда соответствие
найдено, выполняется тело соответствующей инструкции case
. Порядок инструкций становится важным, когда один или
несколько участвующих в сравнениях типов являются интерфейсами, так как при этом возможна ситуация, когда соответствие
будет найдено в нескольких инструкциях case
. Положение инструкции default
относительно прочих инструкций значения не
имеет. В выборе типа применение fallthrough
не разрешено.
Обратите внимание, что в исходной функции логика для случаев bool
и string
требует доступа к значению, извлеченному
декларацией типа. Так как это достаточно типичная ситуация, инструкция выбора типа имеет расширенную форму, которая в
каждом case
связывает извлекаемое значение с новой переменной:
switch x := x.(type) { /*...*/ }
Здесь новая переменная также названа x
; как и в случае декларации типа, повторное использование имен переменных
является достаточно распространенным. Подобно обычной инструкции switch
, инструкция выбора типа неявно создает
лексический блок, так что объявление новой переменной x
не конфликтует с переменной x
во внешнем блоке.
Каждый case
также неявно создает отдельный лексический блок.
Перепишем sqlQuote
с использованием расширенного выбора типов, что делает код существенно понятнее:
fucn sqlQuote(x interface{}) string {
switch x := x.(type) {
case nil:
return "NULL"
case int, uint:
return fmt.Sprintf("%d", x) // Здесь x имеет тип interface{}.
case bool:
if x {
return "TRUE"
}
return "FALSE"
case string:
return sqlQuoteString(x) // Не показана функция
default:
panic(fmt.Sprintf("непредвиденный тип %T: %v", x, x))
}
}
В этой версии, в блоке каждого case
с единственным типом переменная x
имеет тот же тип, что и указанный в case
.
Например, x
имеет тип bool
в case bool:
и тип string
в case string:
. Во всех остальных случаях x
имеет
(интерфейсный) тип операнда switch
, в данном примере - interface{}
. Когда одно и то же действие требуется для
нескольких case
, таких, как int
и uint
, выбор типа позволяет легко их объединить.
Хотя sqlQuote
принимает аргумент любого типа, функция выполняется до конца, только если тип аргумента соответствует
одному из case
в инструкции выбора типа. В противном случае осуществляется panic
с сообщением непредвиненный тип
.
Хотя типом x
является interface{}
, мы рассматриваем его
как распознаваемое объединение int, uint, bool, string и nil
- Интерфейсы в Go могут использоваться двумя способами: для выражения подобия типов и для объединения типов:
- Первый способ - это когда интерфейс определяет методы, которые должны быть реализованы типами. Это похоже на
полиморфизм подтипов, когда объекты разных типов могут использоваться одинаково благодаря общему
интерфейсу.
type Expr interface { String() string }
; - Второй способ - это когда интерфейс используется как объединение типов. Это позволяет хранить значения разных
типов в одной переменной и обрабатывать их по-разному в зависимости от типа. Это похоже на перегрузку, когда
функция может принимать аргументы разных типов и обрабатывать их
по-разному;
func getType(x interface{}) string {/*...*/}
- Первый способ - это когда интерфейс определяет методы, которые должны быть реализованы типами. Это похоже на
полиморфизм подтипов, когда объекты разных типов могут использоваться одинаково благодаря общему
интерфейсу.
- API Go для работы с базами данных SQL позволяет безопасно создавать запросы, заменяя символы
?
на значения аргументов; - Построение запросов таким образом позволяет избежать атак SQL-инъекций. Метод
Exec
преобразует значение каждого аргумента в его SQL-запись в виде литерала; - Инструкция
switch
в Go упрощает написание цепочекif-else
. Аналогично,type switch
упрощает написание цепочекif-else
для проверки типов; Type switch
- это инструкцияswitch
, которая проверяет динамический тип значения интерфейса;- Она выглядит как обычная инструкция
switch
, но вместо значения используетсяx.(type)
, гдеx
- это переменная интерфейса,type
представляет собой ключевое слово; - Каждый
case
указывает один или несколько типов. Если тип значенияx
соответствует типу вcase
, то выполняется тело этогоcase
; - Если ни один
case
не соответствует типу значенияx
, то выполняется телоdefault
(если оно есть). - Все инструкции
case
рассматриваются по порядку, и когда соответствие найдено, выполняется тело соответствующей инструкцииcase
. Однако, порядокcase
имеет значение вtype switch
, если несколькоcase
могут соответствовать типу значенияx
. Например, если у нас есть интерфейсinterface{}
и дваcase
:case int
иcase interface{int}
. Если значениеx
имеет типint
, то обаcase
могут соответствовать этому типу. В этом случае будет выполненпервый case
в порядке следования. Поэтому порядокcase
важен при написанииtype switch
; - Использование
fallthrough
запрещено вtype switch
; - Можно использовать расширенную форму
type switch
, чтобы связать извлеченное значение с новой переменной в каждомcase
. Это позволяет получить доступ к значению, извлеченному декларацией типа.switch x := x.(type)
. Эта новая переменная может иметь то же имя, что и переменная интерфейсаx
; - В каждом
case
с единственным типом новая переменная имеет этот тип.case int: x // x.(type) == int
; - В
type switch
можно объединить несколькоcase
, если требуется выполнить одно и то же действие для нескольких типов (case int, uint:
); - Хотя типом переменной
x
являетсяinterface{}
, ее можно рассматривать какобъединение типов
, которые могут соответствовать ее значению. Например, если мы знаем, что значениеx
может бытьint, uint, bool, string или nil
, то можем рассматриватьx
как объединение этих типов.