Go lang

В Go nil — это значение, которое могут принимать только ссылочные типы (указатели, слайсы, карты, каналы, интерфейсы и функции). Скалярные типы (числа, логические значения) всегда имеют дефолтное значение (0, false), которое не является «пустотой» в смысле nil.

Если вам критически важно иметь возможность присвоить nil «числу», у вас есть три пути, сохраняющих строгую типизацию:

  1. Указатель на тип (*int, *bool): Это самый распространенный способ. Переменная хранит либо адрес в памяти с числом, либо nil.
  2. Типы из пакета sql (sql.NullInt64 и др.): Это структуры, содержащие само значение и флаг Valid. Они явно разделяют «ноль» и «пусто».
  3. Собственные обертки на Generics: Вы можете создать структуру Option[T], которая будет строго типизирована под конкретный скаляр, но уметь хранить состояние «пусто».

Важное уточнение: Присвоение nil скаляру (var x int = nil) вызовет ошибку компиляции, так как компилятор Go строго следит за тем, чтобы типы, не являющиеся указателями, всегда имели валидное значение в памяти.

Переменная vs Структура vs Интерфейс

// Структура имеет тип:
type T struct {
	S string
}
var t *T
fmt.Printf("%v, %T\n", t, t) // <nil>, *main.T

// Интерфейс не имеет тип, убедимся в этом:
type I interface {
	// не важно, объявлены тут методы или нет, переменная созданная от интерфейса равна nil
}
var i I
fmt.Printf("%v, %T\n", i, i) // <nil>, <nil>

// Утиная типизация позволяет указывать значение и менять тип на примитивы:
i = 42
fmt.Printf("%v, %T\n", i, i) // 42, int

// Утиная типизация позволяет указывать значение и менять тип даже на структуры:
i = t
fmt.Printf("%v, %T\n", i, i) // <nil>, *main.T

// Утиная типизация позволяет указывать значение и менять тип даже на указатель:
i = &T{"hello"}
fmt.Printf("%v, %T\n", i, i) // &{hello}, *main.T

Переменные можно создавать сразу списком: x, y := 0, 1

Продолжим работу с типами:

// Создаем переменную пустого интерфейса.
// В неё можно положить значение любого типа, кладем строку "hello":
var i interface{} = "hello"
fmt.Println(i) // hello

// Говорим компилятору: «Я уверен, что внутри i лежит строка, достань её и положи в s»
s := i.(string)
fmt.Println(s) // hello

// Проверим тип переменной:
s, ok := i.(string)
fmt.Println(s, ok) // hello true - так как типы совпали

// Проверим тип переменной:
f, ok := i.(float64)
fmt.Println(f, ok) // 0 false - так как типы НЕ совпали

// Проверять тип переменной не будем и получим панику:
f = i.(float64) // panic: interface conversion: interface {} is string, not float64

Значение vs Указатель

package main

import "fmt"

type Person struct {
    Name string
}

func main() {
    a := Person{"Bob"} // a имеет тип данных {Bob} — это сами данные
    b := &Person{"Bob"} // b имеет тип данных &{Bob} — это адрес на данные, за ними нужно еще сходить
    fmt.Println(a, b)
}

Чтобы прочитать значение из памяти, процессору нужен адрес:

  1. Для a (значение): Адрес этой переменной известен компилятору заранее. Когда ты пишешь a.Name, процессор сразу идет по прямому адресу и берет данные. Это одно обращение к памяти.
  2. Для b (указатель):
    • Сначала процессор обращается к памяти, где лежит переменная b, чтобы прочитать хранящийся в ней адрес.
    • Затем он берет этот адрес и идет по нему за данными.
    • Это два обращения к памяти (так называемое разыменование).

Вот как распределяется память:

  1. Переменная a: Потребляет ровно столько, сколько весит сама структура (в твоем случае это размер строки string).
  2. Переменная b: Потребляет память под саму структуру ПЛЮС память под хранение адреса (указателя). На 64-битных системах указатель занимает дополнительные 8 байт.

Передача адреса (указателя):

// принимаем указатель на данные:
func MyFun1(v *Vertex) {
	v.X = 3 // меняем оригинал
}
// принимаем копию данных:
func MyFun2(v Vertex) {
	v.X = 4 // меняем копию
}
func (v *Vertex) MyMethod() {
	v.X = 5
}
v := Vertex{1, 2}
MyFun1(&v) // передаем ссылку на значение, теперь v = {3 2}
MyFun2(v) // передаем копию значения, v не меняется
v.MyMethod() // теперь v = {5 2}

Массив

var s []int // переменная имеет значение nil
var a [2]string // массив из двух пустых строк
a[0] = "Hello"
a[1] = "World"

или так:

primes := [6]int{2, 3, 5, 7, 11, 13}

var s []int = primes[1:4]
fmt.Println(s)

Создание массива структуры и сразу заполнение ее:

s := []struct {
    i int
    b bool
}{
    {2, true},
    {3, false},
    {5, true},
    {7, true},
    {11, false},
    {13, true},
}
fmt.Println(s) // [{2 true} {3 false} {5 true} {7 true} {11 false} {13 true}]

Create a tic-tac-toe board:


board := [][]string{
    []string{"_", "_", "_"},
    []string{"_", "_", "_"},
    []string{"_", "_", "_"},
}
fmt.Println(board, len(board), cap(board)) // [[_ _ _] [_ _ _] [_ _ _]] 3 3

// пробежимся по массиву (аналог foreach):
var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}
for i, v := range pow {
	...
}

Кстати: []int - это тип данных!

Еще пример перебора данных:

type IPAddr [4]byte

hosts := map[string]IPAddr{
    "loopback":  {127, 0, 0, 1},
    "googleDNS": {8, 8, 8, 8},
}
for name, ip := range hosts {
    fmt.Printf("%v: %v\n", name, ip)
}
получим:
googleDNS: [8 8 8 8]
loopback: [127 0 0 1]

Slice - кусочек массива

	// Предположим мы создали холодильник с продуктами:
	s := []int{2, 3, 5, 7, 11, 13}
	fmt.Println(s, len(s), cap(s)) // [2 3 5 7 11 13] 6 6

	// Применяем Slice-операцию к массиву - мы говорим массиву: считай что в тебе теперь 2 продукта
	s = s[:2] // подрезаем до 2-х первых
	fmt.Println(s, len(s), cap(s)) // [2 3] 2 6

	// Применяем Slice-операцию к массиву - мы говорим массиву: считай что в тебе теперь 4 продукта
	s = s[:4] // подрезаем до 4-х первых
	fmt.Println(s, len(s), cap(s)) // [2 3 5 7] 4 6

// make - тоже может создать слайс:
s := make([]int, 0, 1000) // len=0, cap=1000

Чтобы реально «потерять» данные, нужно сдвинуть начало: s = s[2:]. Тогда прочитать 1-ое значение уже не получится.

Map - ассоциативный массив

Если просто объявить карту var m map[string]int, она будет равна nil. Попытка записать в неё данные вызовет краш (panic).

m := make(map[string]int)
m["key"] = 10
fmt.Println(m["key"])

// Предположим есть структура
type Vertex struct {
	Lat, Long float64
}

// список объектов Vertex, с доступом по ключам:
var m map[string]Vertex

// Пример создания карты без функции make:
var m = map[string]Vertex{
	"Bell Labs": {40.68433, -74.39967},
	"Google":    {37.42202, -122.08408},
}
fmt.Println(m) // map[Bell Labs:{40.68433 -74.39967} Google:{37.42202 -122.08408}]

// добавляем и удаляем ключи:
m := make(map[string]int)

m["Answer"] = 42
fmt.Println(m["Answer"]) // 42

delete(m, "Answer")
fmt.Println(m["Answer"]) // 0

v, ok := m["Answer"]
fmt.Println(v, ok) // 0 false - где false означет отсутсвие

Заметка: литералы карт идентичны литералам структур, но ключи в них обязательны.

Функция

func compute(fn func(float64, float64) float64) float64 {
	return fn(3, 4)
}
// функция может быть анонимной:
hypot := func(x, y float64) float64 {
	return math.Sqrt(x*x + y*y)
}
fmt.Println(hypot(5, 12)) // 13
fmt.Println(compute(hypot)) // 5

func adder() func(int) int {
	sum := 0
	// Функция становится замыканием, когда обращается к внешней переменной:
	return func(x int) int {
		sum += x
		return sum
	}
}

Метод

// функция:
func Abs(v Vertex) float64 {
	return ...
}

// метод:
func (v Vertex) Abs() float64 {
	return ...
}

// доступ к объекту (v) и переменной (f) одновременно:
func (v *Vertex) Scale(f float64) {
	v.X = v.X * f
}
v := Vertex{3, 4}
v.Scale(10)

Свой тип данных

Можно создавать свои типы на базе примитивов:

type MyFloat float64
f := MyFloat(1.2)
fmt.Println(f) // 1.2

Удивительно, но к своим типам данных можно создавать методы:

func (f MyFloat) Abs() float64 {
	if f < 0 {
		return float64(-f)
	}
	return float64(f)
}

Интерфейс

Пример несовпадения типа при присвоении:

package main

import (
	"fmt"
)

type Vertex struct {
	X, Y float64
}

type MyFloat float64

type Abser interface {
	Abs() float64
}

func (f MyFloat) Abs() float64 {
	return 1.1
}

func (v *Vertex) Abs() float64 {
	return 1.1
}

func main() {
	f := MyFloat(1) // указываем 1 как значение
	v := Vertex{3, 4}

	var a Abser
	a = f  // a MyFloat implements Abser
	a = &v // a *Vertex implements Abser
	a = v  // ERROR: v does not implement Abser
}

Implicit interface (неявный интерфейс) - забавно, что наличие интерфейса ни к чему не обязывает:

package main

import "fmt"

type I interface {
	M()
}

type T struct {
	S string
}

func (t T) M() {
	fmt.Println(t.S)
}

func main() {
	var i I = T{"hello"}
	i.M() // печатает hello
}

Go имеет внутри себя следующий интерфейс:

type Stringer interface {
    String() string
}

Он говорит, что если данные содержат метод String(), то данные могут форматироваться в строку, пример:

type Person struct {
	Name string
	Age  int
}

func (p Person) String() string {
	return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
}

a := Person{"Arthur", 42}
z := Person{"Zaphod", 9001}
fmt.Println(a, z) // Arthur (42 years) Zaphod (9001 years)

Еще имеется интерфейс-ошибка:

type error interface {
    Error() string
}
// пример применения:
i, err := strconv.Atoi("42")
if err != nil {
    ...
}

Осторожнее: error interface можно случайно создать в своей структуре:

type MyError struct {
	When time.Time
	What string
}
// опа 1 – теперь MyError это частный случай "error interface":
func (e *MyError) Error() string {
	return fmt.Sprintf("at %v, %s", e.When, e.What)
}
// опа 2 – указав возвращаемый тип error мы сказали, что возвращается ошибка:
func run() error {
	return &MyError{time.Now(), "it didn't work",}
}
if err := run(); err != nil {
	fmt.Println(err) // at 2009-11-10 23:00:00 +0000, it didn't work
}

 

Свич

i.(type) – специальная форма, которую можно использовать только внутри switch

package main

import "fmt"

func do(i interface{}) {
	switch v := i.(type) {
	case int:
		fmt.Printf("It is an integer\n")
	case string:
		fmt.Printf("It is a string\n")
	default:
		fmt.Printf("I don't know about type %T!\n", v)
	}
}

func main() {
	do(21) // It is an integer
	do("hello") // It is a string
	do(true) // I don't know about type bool!
}

Generics (обобщённое программирование)

Синтаксис [T comparable] — это способ сказать Go: «эта функция умеет работать с любым типом данных, лишь бы его можно было сравнивать через ==».

func Index[T comparable](s []T, x T) int {
	for i, v := range s {
		if v == x {
			return i
		}
	}
	return -1
}

si := []int{10, 20, 15, -10}
fmt.Println(Index(si, 15))

Помимо обобщенных функций, Go также поддерживает обобщенные типы:

type List[T any] struct {
	next *List[T]
	val  T
}

Список представляет собой односвязный список, содержащий значения любого типа.

Goroutines

func say(s string) {
	for i := 0; i < 5; i++ {
		time.Sleep(100 * time.Millisecond)
		fmt.Println(s)
	}
}

func main() {
	go say("world") // напечатает от 0 до 5 раз (потому что горутины работают пока работает ф-я main
	say("hello") // напечатает ровно 5 раз
}

Channels

Каналы представляют собой типизированный трубопровод (typed conduit):

ch <- v // Отправить v на канал ch (данные поступают в направлении стрелки)
v := <-ch // Получение данных от ch и присвоить значение переменной v

Подобно картам и срезам, каналы необходимо создать перед использованием:

ch := make(chan int)

По умолчанию отправка и получение блокируются до тех пор, пока другая сторона не будет готова. Это позволяет горутинам синхронизироваться без явных блокировок или переменных условий.

Buffered Channels

Отличается от обычного канала тем, что данный канал при переполнении завершает Go-приложение с ошибкой, пример:

func main() {
	ch := make(chan int, 1) // создаем канал с int-ами
	ch <- 1
	ch <- 2
	fmt.Println(<-ch)
	fmt.Println(<-ch)
}
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
	/tmp/sandbox665068414/prog.go:8 +0x47

С каналами так же как с слайсами, можно использовать функции:

  • len(s) показывает, сколько элементов в окне сейчас.
  • cap(s) показывает, сколько элементов всего может влезть

Обратите внимание на типизацию канала в следующем примере:

func fibonacci(n int, c chan int) {
	x, y := 0, 1
	for i := 0; i < n; i++ {
		c <- x
		x, y = y, x+y
	}
	close(c)
}

func main() {
	c := make(chan int, 10)
	go fibonacci(cap(c), c)
	for i := range c {
		fmt.Println(i)
	}
}

Select - разберем по косточкам:

func fibonacci(c, quit chan int) {
    x, y := 0, 1 // Инициализация первых двух чисел Фибоначчи
    for {        // Бесконечный цикл
        select { // Ждем, какая из веток (case) сработает первой
        // Жду, когда кто-нибудь захочет прочитать из канала c. Как 
        // только это случится — я запишу туда текущее значение x:
        case c <- x: // это режим отправителя
            // Если кто-то готов прочитать из канала 'c', отправляем туда x
            // и вычисляем следующую пару чисел
            x, y = y, x+y
        // Жду, когда кто-нибудь запишет что угодно в канал quit. Как только там 
        // что-то появится — я это заберу (выну из канала) и выполню код ниже
        case <-quit: // это режим получателя
            // Если в канал 'quit' пришло сообщение (любое число), 
            // печатаем "quit" и выходим из функции (завершаем цикл)
            fmt.Println("quit")
            return
        }
    }
}
func main() {
    c := make(chan int)    // Создаем канал для передачи чисел Фибоначчи
    quit := make(chan int) // Создаем канал для сигнала об остановке

    // Запускаем анонимную горутину («потребителя»)
    go func() {
        for i := 0; i < 10; i++ {
            // 10 раз читаем число из канала 'c' и печатаем его
            // Каждый раз, когда мы здесь читаем (<-c), 
            // в функции fibonacci срабатывает первый 'case'
            fmt.Println(<-c)
        }
        // После 10 чисел отправляем 0 в канал 'quit',
        // чтобы остановить бесконечный цикл в fibonacci
        quit <- 0
    }()

    // Вызываем функцию в основной горутине. 
    // Она будет работать, пока не получит сигнал в канал 'quit'
    fibonacci(c, quit)
}

Продолжение следует - https://go.dev/tour/concurrency/6


11.09.2021 07:08

Комментарии

+
Irina |

Copyrights © 2007 - 2026 YaPro.Ru