Skip to content

Latest commit

 

History

History

lesson13

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 

7.13 Выбор типа

Интерфейсы используются двумя различными способами. В первом из них, примерами которого являются 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 как объединение этих типов.