В Go nil — это значение, которое могут принимать только ссылочные типы (указатели, слайсы, карты, каналы, интерфейсы и функции). Скалярные типы (числа, логические значения) всегда имеют дефолтное значение (0, false), которое не является «пустотой» в смысле nil.
Если вам критически важно иметь возможность присвоить nil «числу», у вас есть три пути, сохраняющих строгую типизацию:
*int, *bool): Это самый распространенный способ. Переменная хранит либо адрес в памяти с числом, либо nil.sql (sql.NullInt64 и др.): Это структуры, содержащие само значение и флаг Valid. Они явно разделяют «ноль» и «пусто».Option[T], которая будет строго типизирована под конкретный скаляр, но уметь хранить состояние «пусто».Важное уточнение: Присвоение nil скаляру (var x int = nil) вызовет ошибку компиляции, так как компилятор Go строго следит за тем, чтобы типы, не являющиеся указателями, всегда имели валидное значение в памяти.
// Структура имеет тип:
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 float64package main
import "fmt"
type Person struct {
Name string
}
func main() {
a := Person{"Bob"} // a имеет тип данных {Bob} — это сами данные
b := &Person{"Bob"} // b имеет тип данных &{Bob} — это адрес на данные, за ними нужно еще сходить
fmt.Println(a, b)
}Чтобы прочитать значение из памяти, процессору нужен адрес:
a (значение): Адрес этой переменной известен компилятору заранее. Когда ты пишешь a.Name, процессор сразу идет по прямому адресу и берет данные. Это одно обращение к памяти.b (указатель):b, чтобы прочитать хранящийся в ней адрес.Вот как распределяется память:
a: Потребляет ровно столько, сколько весит сама структура (в твоем случае это размер строки string).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] // Предположим мы создали холодильник с продуктами:
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-ое значение уже не получится.
Если просто объявить карту 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!
}Синтаксис [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
}Список представляет собой односвязный список, содержащий значения любого типа.
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 раз
}Каналы представляют собой типизированный трубопровод (typed conduit):
ch <- v // Отправить v на канал ch (данные поступают в направлении стрелки)
v := <-ch // Получение данных от ch и присвоить значение переменной vПодобно картам и срезам, каналы необходимо создать перед использованием:
ch := make(chan int)По умолчанию отправка и получение блокируются до тех пор, пока другая сторона не будет готова. Это позволяет горутинам синхронизироваться без явных блокировок или переменных условий.
Отличается от обычного канала тем, что данный канал при переполнении завершает 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
Комментарии