Зачем использовать interface{} с осторожностью в Go

задняя часть Go

Помню, когда я впервые перешел с Java на Go, старший, пользовавшийся языком Go, сказал мне: «Тебе следует меньше использовать interface{}, эта штука очень полезная, но лучше ею не пользоваться». Лидер пошутил: «Нет, он пришел с Java и хочет определить класс, когда сталкивается с проблемой». В то время мое первое впечатление об интерфейсе{} было в том, что он аналогичен классу Object в Java. определенно не распространять его повсюду, когда мы используем Java.Объект ах. Более поздние факты доказали, что молодые люди все-таки молодые люди.Глядя на летающий интерфейс{} в текущем проекте, они иногда становятся параметрами функций, что вызывает недоумение, иногда они скрыты в полях структуры и бесконечно меняются. Я не могу не вспомнить фразу, которую видел раньше: «Динамический язык — это круто какое-то время, крематорий рефакторинга кода».

1. Яма трансформации объекта интерфейса{}

После того, как язык использовался в течение длительного времени, он неизбежно повлияет на мышление пользователя. Как одна из важных функций Go, интерфейс {} представляет собой аналогичный*voidУказатель, который может указывать на разные типы данных. Таким образом, мы можем использовать его для указания на любые данные, что обеспечивает удобство, аналогичное динамическим языкам, как в следующем примере:

type BaseQuestion struct{
    QuestionId int
    QuestionContent string
}

type ChoiceQuestion struct{
    BaseQuestion
    Options []string
}

type BlankQuestion struct{
    BaseQuestion
    Blank string
}

func fetchQuestion(id int) (interface{} , bool) {
    data1 ,ok1 := fetchFromChoiceTable(id) // 根据ID到选择题表中找题目,返回(ChoiceQuestion)
    data2 ,ok2 := fetchFromBlankTable(id)  // 根据ID到填空题表中找题目,返回(BlankQuestion)
    
    if ok1 {
        return data1,ok1
    }
    
    if ok2 {
        return data2,ok2
    }
    
    return nil ,false
}

В приведенном выше коде data1ChoiceQuestionтип, данные2BlankQuestionтип. Поэтому наш интерфейс{} относится к трем типам, а именноChoiceQuestion,BlankQuestionиnil, что отражает разницу между Go и объектно-ориентированными языками.В объектно-ориентированных языках мы могли бы написать:

func fetchQuestion(id int) (BaseQuestion , bool) {
    ...
}

Просто верните базовый классBaseQuestionТо есть методы или поля, которые должны использоваться в подклассах, должны только понижаться. Однако в Go такого нет.is-Aконцепции, код будет безжалостно подсказывать вам, что тип возвращаемого значения не совпадает.
Итак, как мы используем этоinterface{}Что касается возвращаемого значения, мы не знаем, какого оно типа. Итак, вы должны взять на себя труд судить по одному:

func printQuestion(){
    if data, ok := fetchQuestion(1001); ok {
		switch v := data.(type) {
		case ChoiceQuestion:
			fmt.Println(v)
		case BlankQuestion:
			fmt.Println(v)
		case nil:
			fmt.Println(v)
		}
		fmt.Println(data)
	}
}

// ------- 输出--------
{{1001 CHOICE} [A B]}
data -  &{{1001 CHOICE} [A B]}

RU, как будто через Goswitch-typeО синтаксическом сахаре не очень сложно судить. Если вы так думаете и используете этот метод, как и я, поздравляю, вы попали в яму.
Поскольку спрос постоянно меняется, если спрос есть сейчас, его нужноChoiceQuesitonПри печати дайтеQuestionContentпрефикс поля选择题, поэтому код становится следующим:

func printQuestion() {
	if data, ok := fetchQuestion(1001); ok {
		switch v := data.(type) {
		case ChoiceQuestion:
		    v.QuestionContent = "选择题"+ v.QuestionContent
			fmt.Println(v)
			
		...
		fmt.Println(data)
	}
}

// ------- 输出--------
{{1001 选择题CHOICE} [A B]}
data -  {{1001 CHOICE} [A B]}

Мы получаем другой результат, и данные вообще не меняются. Как могли догадаться некоторые читатели,vиdataВообще не указывая на одни и те же данные, другими словами,v := data.(type)Этот оператор создаст новые данные в соответствующемtypeскопируйте ниже, у нас естьvОперации не влияют на данные. Конечно, мы можем спроситьfetchFrom***Table()возвращение*ChoiceQuestionтип, так что мы можем судить по*ChoiceQuestionЧтобы решить проблему копирования данных:

func printQuestion() {
	if data, ok := fetchQuestion(1001); ok {
		switch v := data.(type) {
		case *ChoiceQuestion:
		    v.QuestionContent = "选择题"+ v.QuestionContent
			fmt.Println(v)
		...
		fmt.Println(data)
	}
}
// ------- 输出--------
&{{1001 选择题CHOICE} [A B]}
data -  &{{1001 选择题CHOICE} [A B]}

Однако в реальных проектах у вас может быть много причин не двигаться.fetchFrom***Table(), возможно, у вас нет прав на изменение функций работы с базой данных, возможно, этот метод используется во многих местах проекта, и вы не можете его случайно изменить. вот это тоже я не писалfetchFrom***Table()По соображениям реализации, во многих случаях, эти методы могут быть закрыты только для вас. Сделав шаг назад, даже если сигнатура метода может быть изменена, мы перечисляем здесь только два типа вопросов, могут быть существенные вопросы, вопросы по чтению, вопросы по написанию и т. д.QuestonContentДобавьте соответствующий префикс типа вопроса, разве мы не хотим написать следующий код:

func printQuestion() {
	if data, ok := fetchQuestion(1001); ok {
		switch v := data.(type) {
		case *ChoiceQuestion:
		    v.QuestionContent = "选择题"+ v.QuestionContent
		    fmt.Println(v)
		case *BlankQuestion:
		    v.QuestionContent = "填空题"+ v.QuestionContent
		    fmt.Println(v)
		case *MaterialQuestion:
		    v.QuestionContent = "材料题"+ v.QuestionContent
		    fmt.Println(v)
		case *WritingQuestion:
		    v.QuestionContent = "写作题"+ v.QuestionContent
		    fmt.Println(v)
		... 
		case nil:
		    fmt.Println(v)
		fmt.Println(data)
	}
}

Этот тип кода содержит много повторяющихся структур, как видно,interface{}Динамические характеристики структуры данных не подходят для сложных структур данных, неужели нельзя иметь более удобные операции? Когда горы и реки иссякнут, может быть, мы сможем оглянуться на объектно-ориентированное мышление, может быть, наследование и полиморфизм смогут очень хорошо решить проблемы, с которыми мы сталкиваемся.

Мы можем извлечь эти типы вопросов в интерфейс и позволитьBaseQuestionреализовать этот интерфейс.

type IQuestion interface{
    GetQuestionType() int
    GetQuestionContent()string
    AddQuestionContentPrefix(prefix string)
}

type BaseQuestion struct {
	QuestionId      int
	QuestionContent string
	QuestionType    int
}

func (self *BaseQuestion) GetQuestionType() int {
	return self.QuestionType
}

func (self *BaseQuestion) GetQuestionContent() string {
	return self.QuestionContent
}

func (self *BaseQuestion) AddQuestionContentPrefix(prefix string) {
	self.QuestionContent = prefix + self.QuestionContent
}

//修改返回值为IQuestion
func fetchQuestion(id int) (IQuestion, bool) {
	data1, ok1 := fetchFromChoiceTable(id) // 根据ID到选择题表中找题目
	data2, ok2 := fetchFromBlankTable(id)  // 根据ID到选择题表中找题目

	if ok1 {
		return &data1, ok1
	}

	if ok2 {
		return &data2, ok2
	}

	return nil, false
}

Независимо от того, сколько существует типов вопросов, главное, чтобы они содержалиBaseQuestion, это может быть реализовано автоматическиIQuestionИнтерфейс, таким образом, мы можем управлять данными, определяя методы интерфейса.

func printQuestion() {
	if data, ok := fetchQuestion(1002); ok {
		var questionPrefix string

        //需要增加题目类型,只需要添加一段case
		switch  data.GetQuestionType() {
		case ChoiceQuestionType:
		    questionPrefix = "选择题"
		case BlankQuestionType:
		    questionPrefix = "填空题"
		}

		data.AddQuestionContentPrefix(questionPrefix)
		fmt.Println("data - ", data)
	}
}

//--------输出--------
data -  &{{1002 填空题BLANK 2} [ET AI]}

Этот подход, несомненно, значительно сокращает количество создаваемых реплик и легко масштабируется. На этом примере мы также узнали о силе интерфейса Go, хотя Go не является объектно-ориентированным языком, но благодаря хорошему дизайну интерфейса мы можем полностью шпионить за тенью объектно-ориентированного мышления. Неудивительно, что в FAQ документации Go дляIs Go an object-oriented language?Официальный ответ на этот вопросyes and no.
Здесь есть что сказать, как я уже говорил ранее.v := data.(type)Этот оператор копирует копию данных, но когда данные являются объектом интерфейса, этот оператор является переходом между интерфейсами, а не копией копии данных.

//定义新接口
type IChoiceQuestion interface {
	IQuestion
	GetOptionsLen() int
}

func (self *ChoiceQuestion) GetOptionsLen() int {
	return len(self.Options)
}

func showOptionsLen(data IQuestion) {
    //choice和data指向同一份数据
	if choice, ok := data.(IChoiceQuestion); ok {
	    fmt.Println("Choice has :", choice.GetOptionsLen())
	}
}

//------------输出-----------
Choice has : 2

2. Интерфейс{}nilяма

Посмотрите на следующий код:

func fetchFromChoiceTable(id int) (data *ChoiceQuestion) {
	if id == 1001 {
		return &ChoiceQuestion{
			BaseQuestion: BaseQuestion{
				QuestionId:      1001,
				QuestionContent: "HELLO",
			},
			Options: []string{"A", "B"},
		}
	}
	return nil
}


func fetchQuestion(id int) (interface{}) {
	data1 := fetchFromChoiceTable(id) // 根据ID到选择题表中找题目
	return data1
}

func sendData(data interface{}) {
	fmt.Println("发送数据 ..." , data)
}

func main(){
    data := fetchQuestion(1002)
    
    if data != nil {
        sendData(data)
    }
}

Строка очень распространенных бизнес-кодов, мы запрашиваем вопрос в соответствии с идентификатором, чтобы облегчить будущее расширение, мы используемinterface{}В качестве возвращаемого значения, затем в зависимости от того, являются ли данныеnilопределить, отправлять ли этоQuestion. К сожалению, независимо отfetchQuestion()Метод нашел данные?sendData()будет казнен. бегатьmain(), результат печати выглядит следующим образом:

发送数据 ... <nil>

Process finished with exit code 0

Чтобы понять внутреннюю тайну, нам нужно вспомнитьinterface{}Что это такое?Согласно документации это пустой интерфейс,то есть интерфейс не объявляющий никаких методов.Тогда как интерфейс представлен внутри Go? Я нашел несколько предложений в официальной документации:

Under the covers, interfaces are implemented as two elements, a type and a value. The value, called the interface's dynamic value, is an arbitrary concrete value and the type is that of the value. For the int value 3, an interface value contains, schematically, (int, 3).

Эффект вышеизложенного заключается в том, что интерфейс находится в самом низу Go и представлен как набор значений и типов, соответствующих значениям, характерным для нашего примера кода,fetchQuestion()Возвращаемое значениеinterface{}, на самом деле относится к агрегату (*ChoiceQuestion, data1). Если данные не найдены, то наши data1 равны нулю, и приведенный выше агрегат становится (*ChoiceQuestion, nil). В спецификации Go агрегат такой структуры сам по себе не равен нулю, и, кроме того, только такие агрегаты, как (nil, nil), могут оцениваться как нулевые.

Строго говоря, это неinterface{}Проблема в том, что дизайн интерфейса Go предусматривает, что вы помещаете приведенный выше код вinterface{}Замените его любым другим интерфейсом, который вы определите, и эта проблема возникнет. Итак, наше мнение об интерфейсеnil, должны быть осторожны, если приведенный выше код изменить на форму множественных возвращаемых значений, этой проблемы можно полностью избежать.

func fetchQuestion(id int) (interface{},bool) {
	data1 := fetchFromChoiceTable(id) // 根据ID到选择题表中找题目
	if data1 != nil {
	    return data1,true
	}
	return nil,false
}

func sendData(data interface{}) {
	fmt.Println("发送数据 ..." , data)
}

func main(){
    if data, ok := fetchQuestion(1002); ok {
        sendData(data)
    }
}

Конечно, есть много других решений, которые можно решить самостоятельно.

3. Резюме и цитаты

Я так много написал тут и там, и предисловие немного не к месту, и я извиняюсь за языковой барьер. Как хорошо спроектированный язык, успех Go небезоснователен. Обобщая и обобщая несколько основных проблем, с которыми я столкнулся до сих пор, я постепенно получил немного поверхностное представление о Go. Я сталкивался с подобными проблемами в будущем. Продолжайте добавлять в статью.
interface{}Как самый простой тип интерфейса в Go, он может предоставить нам большое удобство с точки зрения гибкости кода, но мы также должны понимать, что интерфейс — это абстракция класса конкретных вещей, иinterface{}Поскольку интерфейс, который реализует каждая структура, обеспечивает очень высокий уровень абстракции, мы теряем большую часть информации о вещах, поэтому мы используемinterface{}Перед тем, как идти на свидание, надо хорошенько подумать, это как требование перед свиданием вслепую, если ты скажешь, что пока это женщина, я могу это принять, то не вини того, кто придет сюда, может быть высокого роста. , невысокий, толстый, худой, красивый, некрасивый.

Код, который появляется в тексте, можно найти вобразец кодаНайдите полную версию в формате .

EffectiveGo
GoFAQ