ООП, интерфейсы
ООП в go
Согласно предоставленным источникам, Go реализует парадигму объектно-ориентированного программирования (ООП) несколько иначе, чем классические ООП-языки:
Инкапсуляция: Go использует пакеты и видимость полей/методов для инкапсуляции. Поля и методы, начинающиеся с заглавной буквы, являются публичными, а с маленькой - приватными. [1]
Наследование: Go не поддерживает наследование классов в традиционном понимании. Вместо этого используется композиция - встраивание одних структур в другие для расширения функциональности. [1][2]
Полиморфизм: Go реализует полиморфизм через интерфейсы. Интерфейс определяет набор методов, которые должны быть реализованы типом. Тип, реализующий интерфейс, может быть использован везде, где ожидается интерфейс. [2]
Абстракция: Go поощряет использование интерфейсов для определения абстрактных типов и их реализаций. Это позволяет писать более гибкий и модульный код. [3]
Таким образом, Go не следует классическим принципам ООП в полной мере, но предоставляет свои собственные механизмы для реализации ООП-подобных концепций, таких как инкапсуляция, композиция и полиморфизм через интерфейсы. Это делает Go "недо-ООП" языком, но вполне эффективным для разработки объектно-ориентированных приложений. [3]
Citations: [1] https://dzen.ru/a/ZTwY7e7y3T12novC [2] https://habr.com/ru/articles/225907/ [3] https://habr.com/ru/articles/243593/ [4] https://eax.me/go-profiling/ [5] https://habr.com/ru/companies/badoo/articles/301990/
Примеры кода иллюстрирующие ООП в Go
Вот несколько примеров кода, иллюстрирующих реализацию ООП-концепций в Go:
Инкапсуляция:
package main
type person struct {
name string
age int
}
func (p *person) getName() string {
return p.name
}
func (p *person) setName(name string) {
p.name = name
}
func main() {
p := &person{name: "John", age: 30}
fmt.Println(p.getName()) // Выведет "John"
p.setName("Jane")
fmt.Println(p.getName()) // Выведет "Jane"
}
Композиция:
package main
type animal struct {
name string
}
func (a *animal) speak() {
fmt.Printf("%s говорит\n", a.name)
}
type dog struct {
animal
breed string
}
func main() {
d := &dog{animal: animal{name: "Buddy"}, breed: "Labrador"}
d.speak() // Выведет "Buddy говорит"
}
Полиморфизм:
package main
type speaker interface {
speak()
}
type dog struct {
name string
}
func (d *dog) speak() {
fmt.Printf("%s гавкает\n", d.name)
}
type cat struct {
name string
}
func (c *cat) speak() {
fmt.Printf("%s мяукает\n", c.name)
}
func main() {
var s speaker
s = &dog{name: "Buddy"}
s.speak() // Выведет "Buddy гавкает"
s = &cat{name: "Kitty"}
s.speak() // Выведет "Kitty мяукает"
}
Эти примеры демонстрируют, как в Go реализуются ООП-концепции, такие как инкапсуляция, композиция и полиморфизм, с использованием структур, методов и интерфейсов.
В чем разница между make и new?
В языке программирования Go функции make
и new
используются для выделения памяти, но они имеют разные цели и применяются к разным типам данных. Давайте рассмотрим их различия более подробно.
make
make
Функция make
используется для инициализации и выделения памяти для встроенных типов данных, таких как срезы (slices), карты (maps) и каналы (channels). Она возвращает инициализированный (но не нулевой) объект указанного типа.
Примеры использования make
:
Срезы (slices):
slice := make([]int, 5) // Создает срез длиной 5 и емкостью 5
Карты (maps):
m := make(map[string]int) // Создает пустую карту
Каналы (channels):
ch := make(chan int) // Создает небуферизованный канал
new
new
Функция new
используется для выделения памяти для любого типа данных и возвращает указатель на нулевое значение этого типа. Она не инициализирует объект, а просто выделяет память и возвращает указатель на нее.
Примеры использования new
:
Примитивные типы:
p := new(int) // Создает указатель на int с нулевым значением
Структуры:
type MyStruct struct { Field1 int Field2 string } s := new(MyStruct) // Создает указатель на MyStruct с нулевыми значениями полей
Сравнение make
и new
make
и new
Типы данных:
make
используется только для срезов, карт и каналов.new
может использоваться для любого типа данных.
Возвращаемое значение:
make
возвращает инициализированный объект (срез, карту или канал).new
возвращает указатель на нулевое значение указанного типа.
Инициализация:
make
инициализирует объект, готовый к использованию.new
просто выделяет память и возвращает указатель на нее, не инициализируя объект.
Примеры для сравнения
Пример с использованием make
для среза:
package main
import "fmt"
func main() {
slice := make([]int, 5) // Создает срез длиной 5 и емкостью 5
fmt.Println(slice) // Вывод: [0 0 0 0 0]
}
Пример с использованием new
для структуры:
package main
import "fmt"
type MyStruct struct {
Field1 int
Field2 string
}
func main() {
s := new(MyStruct) // Создает указатель на MyStruct с нулевыми значениями полей
fmt.Println(s) // Вывод: &{0 }
}
Заключение
Используйте
make
для создания и инициализации срезов, карт и каналов.Используйте
new
для выделения памяти для любого типа данных и получения указателя на нулевое значение этого типа.
Понимание различий между make
и new
поможет вам правильно использовать их в зависимости от ваших потребностей в управлении памятью и инициализации объектов в Go.
Что такое интерфейсы в Go?
Интерфейсы в Go представляют собой тип, который определяет набор методов, которые должны быть реализованы другими типами. Они позволяют абстрагироваться от конкретной реализации и работать с объектами через их методы. Вот пример кода, иллюстрирующий использование интерфейсов в Go:
package main
import "fmt"
// Определяем интерфейс Animal с методом Speak
type Animal interface {
Speak()
}
// Определяем структуру Dog, реализующую метод Speak
type Dog struct{}
func (d Dog) Speak() {
fmt.Println("Гав-гав!")
}
// Определяем структуру Cat, реализующую метод Speak
type Cat struct{}
func (c Cat) Speak() {
fmt.Println("Мяу-мяу!")
}
func main() {
// Создаем экземпляры структур Dog и Cat
dog := Dog{}
cat := Cat{}
// Полиморфизм: используем интерфейс Animal для вызова метода Speak
animals := []Animal{dog, cat}
for _, animal := range animals {
animal.Speak()
}
}
В этом примере:
Определяется интерфейс
Animal
с методомSpeak
.Создаются структуры
Dog
иCat
, реализующие методSpeak
.В функции
main
создаются экземплярыDog
иCat
, затем они добавляются в срезAnimal
.Циклом проходятся по срезу и вызывается метод
Speak
для каждого животного.
Таким образом, интерфейсы в Go позволяют создавать гибкий и расширяемый код, используя полиморфизм и абстракцию для работы с различными типами данных через общие методы.
Citations: [1] https://proglib.io/p/samouchitel-po-go-dlya-nachinayushchih-chast-9-struktury-i-metody-interfeysy-ukazateli-osnovy-oop-2024-02-19 [2] https://habr.com/ru/companies/vk/articles/463063/ [3] https://eax.me/go-profiling/ [4] https://www.digitalocean.com/community/tutorials/how-to-use-interfaces-in-go-ru [5] https://habr.com/ru/articles/597461/
Как внутри себя устроен интерфейс в Go?
В языке программирования Go интерфейсы являются мощным и гибким механизмом для определения поведения, которое типы должны реализовать. Интерфейсы в Go устроены таким образом, чтобы быть простыми и эффективными, обеспечивая динамическую типизацию и полиморфизм.
Внутреннее устройство интерфейсов в Go
Интерфейс в Go представляет собой пару значений: конкретное значение и конкретный тип. Эти значения хранятся в структуре, которая называется iface
для пустых интерфейсов (interface{}
) и eface
для интерфейсов с методами.
Структура интерфейса
Пустой интерфейс (
interface{}
):Пустой интерфейс может содержать значение любого типа. Внутренне он представлен структурой
eface
.
type eface struct { _type *_type // Указатель на тип data unsafe.Pointer // Указатель на данные }
Интерфейс с методами:
Интерфейс с методами содержит таблицу методов (vtable), которая указывает на конкретные реализации методов для данного типа. Внутренне он представлен структурой
iface
.
type iface struct { tab *itab // Указатель на таблицу методов data unsafe.Pointer // Указатель на данные } type itab struct { inter *interfacetype // Указатель на тип интерфейса _type *_type // Указатель на конкретный тип hash uint32 // Хэш для быстрого сравнения _ [4]byte fun [1]uintptr // Таблица методов }
Пример интерфейса в Go
Рассмотрим пример интерфейса и его реализации:
package main
import "fmt"
// Определение интерфейса
type Speaker interface {
Speak() string
}
// Реализация интерфейса для типа Person
type Person struct {
Name string
}
func (p Person) Speak() string {
return "Hello, my name is " + p.Name
}
// Реализация интерфейса для типа Dog
type Dog struct {
Name string
}
func (d Dog) Speak() string {
return "Woof! My name is " + d.Name
}
func main() {
var s Speaker
s = Person{Name: "Alice"}
fmt.Println(s.Speak())
s = Dog{Name: "Buddy"}
fmt.Println(s.Speak())
}
Объяснение кода
Определение интерфейса:
Интерфейс
Speaker
определяет один методSpeak
, который возвращает строку.
Реализация интерфейса:
Типы
Person
иDog
реализуют методSpeak
, что позволяет им удовлетворять интерфейсуSpeaker
.
Использование интерфейса:
В функции
main
переменнаяs
типаSpeaker
может содержать значение любого типа, который реализует методSpeak
. В данном примереs
сначала содержит значение типаPerson
, а затем значение типаDog
.
Внутренние детали
Когда переменная s
присваивается значению типа Person
или Dog
, Go создает внутреннюю структуру iface
, которая содержит указатель на таблицу методов (itab
) и указатель на данные (data
). Таблица методов (itab
) содержит указатели на конкретные реализации методов для данного типа.
Заключение
Интерфейсы в Go представляют собой мощный механизм для определения поведения, которое типы должны реализовать. Внутренне интерфейсы устроены как структуры, содержащие указатели на тип и данные, а также таблицы методов для интерфейсов с методами. Это позволяет Go эффективно реализовывать динамическую типизацию и полиморфизм, обеспечивая гибкость и производительность.
Для чего нужен пустой интерфeйс?
В Go пустой интерфейс, обозначаемый как interface{}
, является особым типом интерфейса, который не содержит никаких методов. Это означает, что любой тип в Go удовлетворяет пустому интерфейсу, поскольку для этого не требуется реализовывать какие-либо методы.
Основные особенности пустого интерфейса
Универсальность: Поскольку любой тип удовлетворяет пустому интерфейсу, он может использоваться для хранения значений любого типа.
Гибкость: Пустой интерфейс позволяет создавать функции и структуры, которые могут работать с любыми типами данных.
Примеры использования пустого интерфейса
Хранение значений разных типов
package main
import "fmt"
func main() {
var i interface{}
i = 42
fmt.Println(i) // Output: 42
i = "hello"
fmt.Println(i) // Output: hello
i = true
fmt.Println(i) // Output: true
}
Функции с аргументами пустого интерфейса
package main
import "fmt"
func printValue(v interface{}) {
fmt.Println(v)
}
func main() {
printValue(42) // Output: 42
printValue("hello") // Output: hello
printValue(true) // Output: true
}
Использование пустого интерфейса в структурах
package main
import "fmt"
type Container struct {
value interface{}
}
func main() {
c1 := Container{value: 42}
c2 := Container{value: "hello"}
c3 := Container{value: true}
fmt.Println(c1.value) // Output: 42
fmt.Println(c2.value) // Output: hello
fmt.Println(c3.value) // Output: true
}
Преобразование типов (Type Assertion)
Когда вы работаете с пустым интерфейсом, часто возникает необходимость преобразовать значение обратно в его конкретный тип. Это можно сделать с помощью утверждения типа (type assertion).
Пример утверждения типа
package main
import "fmt"
func main() {
var i interface{} = 42
// Утверждение типа
if v, ok := i.(int); ok {
fmt.Println("Integer:", v) // Output: Integer: 42
} else {
fmt.Println("Not an integer")
}
// Утверждение типа с паникой
v := i.(int)
fmt.Println("Integer:", v) // Output: Integer: 42
// Утверждение типа с неверным типом (вызовет панику)
// v = i.(string) // panic: interface conversion: interface {} is int, not string
}
Пример использования пустого интерфейса с рефлексией
Иногда полезно использовать рефлексию для работы с пустым интерфейсом, особенно когда нужно динамически определять типы и значения.
package main
import (
"fmt"
"reflect"
)
func printTypeAndValue(v interface{}) {
t := reflect.TypeOf(v)
val := reflect.ValueOf(v)
fmt.Printf("Type: %s, Value: %v\n", t, val)
}
func main() {
printTypeAndValue(42) // Output: Type: int, Value: 42
printTypeAndValue("hello") // Output: Type: string, Value: hello
printTypeAndValue(true) // Output: Type: bool, Value: true
}
Заключение
Пустой интерфейс в Go является мощным инструментом, который позволяет создавать гибкие и универсальные функции и структуры. Однако, его использование требует осторожности, особенно при преобразовании типов, чтобы избежать ошибок времени выполнения. Понимание и правильное использование пустого интерфейса может значительно расширить возможности вашего кода в Go.
Что такое рефлексия в Go и для чего она используется, простой пример?
Рефлексия в Go — это механизм, который позволяет программе исследовать и манипулировать своими собственными структурами данных во время выполнения. Это мощный инструмент, который может быть полезен в различных сценариях, таких как сериализация данных, создание универсальных функций и тестирование.
Основные концепции рефлексии в Go
Пакет
reflect
:В Go рефлексия реализована в пакете
reflect
. Основные типы и функции этого пакета позволяют работать с типами и значениями во время выполнения.
Типы
reflect.Type
иreflect.Value
:reflect.Type
представляет тип значения, аreflect.Value
представляет само значение. Эти типы предоставляют методы для получения информации о типах и значениях, а также для их изменения.
Пример использования рефлексии
Рассмотрим простой пример, который демонстрирует использование рефлексии для получения информации о типе и значении переменной.
package main
import (
"fmt"
"reflect"
)
// Пример структуры
type Person struct {
Name string
Age int
}
func main() {
// Создаем экземпляр структуры Person
p := Person{Name: "Alice", Age: 30}
// Получаем reflect.Type и reflect.Value
t := reflect.TypeOf(p)
v := reflect.ValueOf(p)
// Выводим информацию о типе
fmt.Println("Type:", t)
fmt.Println("Kind:", t.Kind())
// Выводим информацию о полях структуры
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
value := v.Field(i)
fmt.Printf("Field: %s, Type: %s, Value: %v\n", field.Name, field.Type, value)
}
// Изменение значения поля через рефлексию
vPtr := reflect.ValueOf(&p).Elem() // Получаем указатель на значение
vPtr.FieldByName("Age").SetInt(35)
fmt.Println("Updated Person:", p)
}
Объяснение кода
Определение структуры:
Определяем структуру
Person
с двумя полями:Name
иAge
.
Создание экземпляра структуры:
Создаем экземпляр структуры
Person
с именем "Alice" и возрастом 30.
Получение
reflect.Type
иreflect.Value
:Используем функцию
reflect.TypeOf
для получения типа переменнойp
и функциюreflect.ValueOf
для получения значения переменнойp
.
Вывод информации о типе:
Выводим тип и вид (kind) переменной
p
. Вид указывает на базовый тип данных (например, struct, int, string и т.д.).
Вывод информации о полях структуры:
Используем метод
NumField
для получения количества полей в структуре и цикл для вывода информации о каждом поле, включая имя, тип и значение.
Изменение значения поля через рефлексию:
Для изменения значения поля через рефлексию необходимо получить указатель на значение с помощью
reflect.ValueOf(&p).Elem()
. Затем используем методFieldByName
для получения поля по имени и методSetInt
для установки нового значения.
Заключение
Рефлексия в Go предоставляет мощные инструменты для работы с типами и значениями во время выполнения. Она может быть полезна в различных сценариях, таких как сериализация данных, создание универсальных функций и тестирование. Однако следует использовать рефлексию с осторожностью, так как она может усложнить код и привести к снижению производительности.
В чем недостатки рефлексии в Go?
Рефлексия в Go предоставляет мощные возможности для работы с типами и значениями во время выполнения, но она также имеет свои недостатки. Вот основные из них:
Недостатки рефлексии в Go
Снижение производительности:
Рефлексия вносит дополнительные накладные расходы на выполнение программы. Операции рефлексии, такие как получение типа или значения, а также изменение значений, могут быть значительно медленнее по сравнению с обычными операциями. Это может привести к снижению производительности, особенно в критически важных участках кода.
Сложность кода:
Код, использующий рефлексию, может быть сложнее для понимания и сопровождения. Рефлексия часто требует использования низкоуровневых операций и может приводить к менее читаемому и более запутанному коду.
Отсутствие статической проверки типов:
Рефлексия обходит статическую проверку типов, что может привести к ошибкам, которые обнаруживаются только во время выполнения. Это увеличивает риск возникновения ошибок и может усложнить отладку.
Безопасность типов:
Использование рефлексии может нарушить безопасность типов, так как она позволяет изменять значения и вызывать методы, которые могут не соответствовать ожидаемым типам. Это может привести к неожиданным ошибкам и поведению программы.
Отсутствие автодополнения и рефакторинга:
Инструменты разработки, такие как автодополнение и рефакторинг, могут не работать корректно с кодом, использующим рефлексию. Это может затруднить разработку и сопровождение кода.
Сложность тестирования:
Код, использующий рефлексию, может быть сложнее тестировать. Поскольку рефлексия позволяет изменять внутренние структуры данных и вызывать методы динамически, это может усложнить написание тестов и проверку корректности работы кода.
Пример, иллюстрирующий недостатки рефлексии
Рассмотрим пример, где использование рефлексии может привести к ошибкам и усложнить код:
package main
import (
"fmt"
"reflect"
)
type Person struct {
Name string
Age int
}
func main() {
p := Person{Name: "Alice", Age: 30}
// Изменение значения поля через рефлексию
v := reflect.ValueOf(&p).Elem()
field := v.FieldByName("Age")
if field.IsValid() && field.CanSet() {
if field.Kind() == reflect.Int {
field.SetInt(35)
} else {
fmt.Println("Field 'Age' is not of type int")
}
} else {
fmt.Println("Cannot set field 'Age'")
}
fmt.Println("Updated Person:", p)
}
Объяснение кода
Получение значения поля через рефлексию:
Используем рефлексию для получения значения поля
Age
структурыPerson
.
Проверка типа и установка значения:
Проверяем, является ли поле валидным и доступным для изменения, а затем проверяем его тип перед установкой нового значения.
Недостатки в примере
Сложность кода:
Код с использованием рефлексии сложнее для понимания и сопровождения по сравнению с обычным кодом, который напрямую изменяет значение поля.
Отсутствие статической проверки типов:
Ошибки, связанные с типами, обнаруживаются только во время выполнения, что увеличивает риск возникновения ошибок.
Снижение производительности:
Операции рефлексии, такие как получение значения поля и его изменение, могут быть медленнее по сравнению с обычными операциями.
Заключение
Рефлексия в Go предоставляет мощные возможности для работы с типами и значениями во время выполнения, но она также имеет свои недостатки, такие как снижение производительности, сложность кода, отсутствие статической проверки типов и безопасность типов. Использование рефлексии следует ограничивать и применять только в тех случаях, когда это действительно необходимо. В большинстве случаев предпочтительнее использовать более простые и безопасные методы работы с типами и значениями.
Есть ли у тебя опыт написания собственных тегов при описании структур?
В Go можно использовать теги структур для добавления метаданных к полям структуры. Эти теги часто используются библиотеками для сериализации и десериализации данных, валидации и других целей. Теги структур записываются в обратных кавычках (`
) после объявления поля.
Пример использования стандартных тегов
Рассмотрим пример использования стандартных тегов для JSON-сериализации:
package main
import (
"encoding/json"
"fmt"
)
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
p := Person{Name: "Alice", Age: 30}
jsonData, _ := json.Marshal(p)
fmt.Println(string(jsonData))
}
Создание и использование собственных тегов
Вы можете создавать и использовать собственные теги для структур. Для этого вам нужно будет написать код, который будет считывать и интерпретировать эти теги. Рассмотрим пример, где мы создаем собственные теги для валидации полей структуры.
Пример: Валидация с использованием собственных тегов
Определение структуры с собственными тегами:
package main
import (
"fmt"
"reflect"
"strconv"
"strings"
)
type Person struct {
Name string `validate:"required"`
Age int `validate:"min=18"`
}
Функция для валидации структуры:
func validateStruct(s interface{}) error {
v := reflect.ValueOf(s)
t := reflect.TypeOf(s)
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
tag := t.Field(i).Tag.Get("validate")
if tag == "" {
continue
}
tags := strings.Split(tag, ",")
for _, t := range tags {
parts := strings.Split(t, "=")
switch parts[0] {
case "required":
if isEmptyValue(field) {
return fmt.Errorf("field %s is required", t.Field(i).Name)
}
case "min":
if field.Kind() == reflect.Int {
minValue, _ := strconv.Atoi(parts[1])
if field.Int() < int64(minValue) {
return fmt.Errorf("field %s should be at least %d", t.Field(i).Name, minValue)
}
}
}
}
}
return nil
}
func isEmptyValue(v reflect.Value) bool {
switch v.Kind() {
case reflect.String, reflect.Array, reflect.Slice, reflect.Map, reflect.Chan:
return v.Len() == 0
case reflect.Bool:
return !v.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return v.Uint() == 0
case reflect.Float32, reflect.Float64:
return v.Float() == 0
case reflect.Interface, reflect.Ptr:
return v.IsNil()
}
return false
}
Использование функции валидации:
func main() {
p := Person{Name: "Alice", Age: 17}
err := validateStruct(p)
if err != nil {
fmt.Println("Validation error:", err)
} else {
fmt.Println("Validation passed")
}
}
Объяснение кода
Определение структуры с тегами:
Структура
Person
имеет два поля:Name
иAge
. ПолеName
имеет тегvalidate:"required"
, а полеAge
— тегvalidate:"min=18"
.
Функция для валидации структуры:
Функция
validateStruct
принимает структуру и проверяет каждый ее тег. Если поле имеет тегrequired
, функция проверяет, что значение поля не пустое. Если поле имеет тегmin
, функция проверяет, что значение поля не меньше указанного минимума.
Функция
isEmptyValue
:Эта вспомогательная функция проверяет, является ли значение поля пустым.
Использование функции валидации:
В функции
main
создается экземпляр структурыPerson
и вызывается функцияvalidateStruct
для проверки значений полей. Если валидация не проходит, выводится сообщение об ошибке.
Заключение
Создание и использование собственных тегов в Go позволяет добавлять метаданные к полям структур и использовать их для различных целей, таких как валидация, сериализация и десериализация данных. Рефлексия в Go предоставляет мощные инструменты для работы с тегами структур, но следует помнить о возможных недостатках рефлексии, таких как снижение производительности и сложность кода.
Что такое дженерики в Go?
Дженерики (generics) в Go — это механизм, который позволяет писать функции и типы, работающие с любыми типами данных, без необходимости дублирования кода для каждого конкретного типа. Дженерики были введены в Go 1.18 и предоставляют возможность создавать более абстрактные и переиспользуемые компоненты.
Основные концепции дженериков в Go
Параметры типов:
Параметры типов позволяют определять функции, структуры и интерфейсы, которые могут работать с любыми типами данных. Параметры типов указываются в квадратных скобках
[]
.
Типовые ограничения (type constraints):
Типовые ограничения позволяют ограничить типы, которые могут быть использованы в качестве параметров типов. Это делается с помощью интерфейсов.
Примеры использования дженериков
Пример 1: Обобщенная функция
package main
import "fmt"
// Обобщенная функция для нахождения минимального значения
func Min[T any](a, b T) T {
if a < b {
return a
}
return b
}
func main() {
fmt.Println(Min(3, 4)) // Работает с int
fmt.Println(Min(3.5, 2.1)) // Работает с float64
fmt.Println(Min("a", "b")) // Работает с string
}
В этом примере функция Min
принимает два параметра типа T
и возвращает значение типа T
. Параметр типа T
может быть любым типом, который поддерживает операцию сравнения <
.
Пример 2: Обобщенная структура
package main
import "fmt"
// Обобщенная структура для хранения пары значений
type Pair[T any, U any] struct {
First T
Second U
}
func main() {
p1 := Pair[int, string]{First: 1, Second: "one"}
p2 := Pair[string, float64]{First: "pi", Second: 3.14}
fmt.Println(p1)
fmt.Println(p2)
}
В этом примере структура Pair
принимает два параметра типа T
и U
, что позволяет создавать пары значений различных типов.
Пример 3: Типовые ограничения
package main
import "fmt"
// Интерфейс для типовых ограничений
type Number interface {
int | int32 | int64 | float32 | float64
}
// Обобщенная функция для нахождения суммы элементов с типовыми ограничениями
func Sum[T Number](numbers []T) T {
var sum T
for _, number := range numbers {
sum += number
}
return sum
}
func main() {
fmt.Println(Sum([]int{1, 2, 3, 4})) // Работает с int
fmt.Println(Sum([]float64{1.1, 2.2, 3.3})) // Работает с float64
}
В этом примере интерфейс Number
используется для ограничения типов, которые могут быть использованы в функции Sum
. Это позволяет функции работать только с числовыми типами.
Заключение
Дженерики в Go позволяют писать более абстрактный и переиспользуемый код, уменьшая дублирование и повышая гибкость. Они особенно полезны для создания обобщенных коллекций, алгоритмов и других компонентов, которые могут работать с различными типами данных.
Last updated