Skip to content

goavengers/go-junior

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

44 Commits
 
 
 
 
 
 
 
 

Repository files navigation

Go Junior

Возможно вы заблудились и искали официальную документацию, но поверьте у нас не хуже - тут есть все-что нужно новичкам, плюс вкусняшечки! 😎

Содержание

  • Основные особенности языка
  • Как установить Go?
  • Управление версиями Go
  • Что дальше?
  • Подводные камни Go для начинающих
    • Переменные
      • Короткие объявления переменных можно использовать только внутри функций
      • Переобъявления переменных с помощью коротких объявлений
      • Нельзя использовать короткие объявления переменных для настройки значений полей
      • Случайное сокрытие переменных
      • Нельзя использовать nil для инициализации переменной без явного указания типа
    • Операторы
    • Константы
    • Срезы и массивы
      • Массивы (slice)
      • Срезы (array)
      • Обновление и привязка значений полей в slice, array
      • Передача массивов в функции
      • Неожиданные значения в выражениях range в слайсах и массивах
    • Карты
      • Порядок итерации карты случайный (на самом деле нет)
      • Проверка наличия ключа карты
      • Карта - это указатель
      • Зачем используют значение пустой стурктуры вместо булева
      • Емкость карты
      • Значения карты не адресуются
      • Гонки данных
      • sync.Map
      • Ёмкость хеш-таблиц
      • Использование nil-слайсов (slice) и хеш-таблиц (map)
    • Строки и байтовые массивы
      • Основы строк в Go
      • Строки не могут быть нулевыми (nil)
      • Строки неизменяемы (вроде)
      • Строки против среза []byte
      • UTF-8 и фсе такое
      • Кодировка строк в Go
      • Руны, что это
      • Оператор строкового индекса или оператор for ... range
    • Циклы песни льда и пламени (нет)
      • Итератор диапазона (range) возвращает два значения
      • Переменные итератора цикла используются повторно
      • Именованные break и continue
    • Погружение в switch и select
      • Операторы case по умолчанию прерываются
      • Именованные break
    • Функции
      • Отложенные (defer)
      • Горутины (goroutines), что это такое **
        • Программа не ждет запущенных горутин
        • Паникующая горутина приведет к сбою всего приложения, вот ***
    • Интерфейсы
    • Наследование (почти ООП. кек)
    • Операторы равенства, проверка на равенство, равенство во всем мире, мы топим за ра.., стоп не туда
    • Менеджмент памяти
    • Логирование
    • Дата и время

Go или GoLang — компилируемый многопоточный язык программирования. Язык был разработан Google для решения проблем корпорации, возникающих при разработке программного обеспечения.

Основные особенности языка:

  • Ортогональность — в языке есть небольшое число средств, не повторяющих функциональность друг друга.
  • Простая грамматика — минимум ключевых слов, легко разбираемая структура и читаемый код.
  • Простая работа с типами — типизация обеспечивает безопасность, но не превращается в бюрократию.
  • Отсутствие неявных преобразований.
  • Сборка мусора.
  • Встроенные простые и эффективные средства распараллеливания.
  • Чёткое разделение интерфейса и реализации.
  • Быстрая сборка за счёт эффективной системы пакетов с явным указанием зависимостей.

Как установить Go?

  1. Скачайте исходники: $ wget https://dl.google.com/go/go$VERSION.$OS-$ARCH.tar.gz

Чтобы понять для какой архитектуры и какую версию Go скачивать посетите сайт: https://golang.org/dl/

  1. Распакуйте: $ tar -C /usr/local -xzf go$VERSION.$OS-$ARCH.tar.gz
  2. Установите переменные окружения.

Для этого откройте ваш .profile файл и добавьте следующие 3 строки в конец файла. Вы можете добавить это в файл .zshrc или .bashrc в соответствии с вашей конфигурацией оболочки.

export GOROOT=/usr/local/go
export GOPATH=$HOME/go
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin

P.S. $ echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.profile

  1. Выполните, чтобы изменения вступили в силу: source ~/.profile или .bashrc, .zshrc - это позволит использовать команды go без перезапуска сеанса.

Управление версиями Go

Используйте goenv, чтобы выбрать версию Go для своего приложения и гарантировать, что ваша среда разработки соответствует производственной.

Что дальше?

Конкурентность

gRPC

Полезные ништячки

Roadmaps

Подвоные камни языка Go для начинающих

Операторы

1. Инкремент и декремент

В отличие от многих других языков, таких как, JS, PHP, Java и т.д., Go не имеет префиксных операторов увеличения (инкремент) или уменьшения (декремент):

var i int
++i // syntax error: unexpected ++, expecting }
--i // syntax error: unexpected --, expecting }

Хотя в Go и есть постфиксные версии этих операторов, но их нельзя использовать в выражениях:

slice := []int{1,2,3}
i := 1
slice[i++] = 0 // syntax error: unexpected ++, expecting :

2. Тернарный оператор

Наверное одна из самых частых вещей, которые ищут разработчики перешедшие с других языков - это тернарный оператор. Но тут все просто - его нет. Разработчики языка Go решили, что этот оператор часто приводит к некрасивому коду, и лучше вообще его не использовать.

iota

  • Ключевое слово iota представляет последовательные целочисленные константы 0, 1, 2,…
  • Оно обнуляется каждый раз, когда const появляется в исходном коде
  • И увеличивается после каждой спецификации const.
const (
    C0 = iota
    C1 = iota
    C2 = iota
)
fmt.Println(C0, C1, C2) // "0 1 2"

Можно упростить до:

const (
    C0 = iota
    C1
    C2
)

Двойное использование йоты не сбрасывает нумерацию:

const (
    C0 = iota // 0
    C1 // 1
    C2 = iota // 2
)

Чтобы начать список констант с 1 вместо 0, можно использовать iota в арифметическом выражении.

const (
    C1 = iota + 1
    C2
    C3
)
fmt.Println(C1, C2, C3) // "1 2 3"

Можно использовать пустой идентификатор, чтобы пропустить значение в списке констант.

const (
    C1 = iota + 1
    _
    C3
    C4
)
fmt.Println(C1, C3, C4) // "1 3 4"

Полный тип enum со строками [best practice]. Вот идиоматический способ реализации перечисляемого типа:

  • создаем новый целочисленный тип,
  • перечисляем его значения с использованием iota,
  • реализуем для типа функцию String.
type Direction int

const (
    North Direction = iota
    East
    South
    West
)

func (d Direction) String() string {
    return [...]string{"North", "East", "South", "West"}[d]
}
// usage

var d Direction = North
fmt.Print(d)
switch d {
case North:
    fmt.Println(" goes up.")
case South:
    fmt.Println(" goes down.")
default:
    fmt.Println(" stays put.")
}
// Output: North goes up.

По стандартному соглашению об именовании, необходимо использовать смешанный caps и для для констант. Например, экспортируемую константу будет правильным назвать NorthWest, а не NORTH_WEST.

Другое распространенное приложение для iota — реализация bitmask. Это небольшой набор булевых значений (их часто называют “флагами”), которые представлены битами в одном числе.

Посмотрите bitmasks и флаги для полного понимания.

В Go срезы и массивы служат аналогичной цели. Они декларируются примерно так же одинаково:

slice := []int{1, 2, 3}
array := [3]int{1, 2, 3}
// let the compiler work out array length
// this will be an equivalent of [3]int
array2 := [...]int{1, 2, 3} 
fmt.Println(slice, array, array2)

Срезы являются более "продвинутыми" и удобными (по факту срез содержит указатель на массив, об этом чуть ниже), по этой причине массивы используют реже и в каких-то специфических случаях. В самых обычных случаях вы также будете использовать срезы.

Массивы

Массив - это типизированный набор памяти фиксированного размера. Массивы разной длины считаются разными несовместимыми типами.

  • В отличие от C, элементы массива инициализируются нулевыми значениями при создании массива, поэтому нет необходимости делать это явно.
  • Также, в отличие от C в Go, массив - это тип значения. Это не указатель на первый элемент блока памяти. Если вы передадите массив в функцию, будет скопирован весь массив. Вы все равно можете передать указатель на массив, чтобы он не копировался.

Срезы

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

Вот так выглядит срез исходном коде Go:

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

Сам срез является типом значения, но он ссылается на массив, который он использует, с помощью указателя. В отличие от массива, если вы передадите срез в функцию, вы получите копию указателя массива, свойств len и cap но данные самого массива не будут скопированы и обе копии (оригинал и в функции) среза будут указывать на один и тот же массив.

То же самое происходит, когда вы "разрезаете" срез. Нарезка создает новый срез, который по-прежнему указывает на тот же массив:

func f1(s []int) {
    // slicing the slice creates a new slice
    // but does not copy the array data
    s = s[2:4] 
    // modifying the sub-slice
    // changes the array of slice in main function as well
    for i := range s {
        s[i] += 10
    }
    fmt.Println("f1", s, len(s), cap(s))
}

func main() {
    s := []int{1, 2, 3, 4, 5}
    // passing a slice as an argument
    // makes a copy of the slice properties (pointer, len and cap)
    // but the copy shares the same array
    f1(s)
    fmt.Println("main", s, len(s), cap(s))
}

Output:

in function f1 [13 14] 2 3
in function main [1 2 13 14 5] 5 5

Мы передали срез в фнкцию, "разрезали" (под-срез) его и изменили значения последнего и, как видно, мы изменили оригинальный срез, который находится на самом верхнем уровне в main.

Чтобы получить копию среза с данными, вам нужно немного поработать. Вы можете вручную скопировать элементы в новый фрагмент или использовать copy или append:

func f1(s []int) {
    s = s[2:4]
    s2 := make([]int, len(s))
    copy(s2, s)

    // or if you prefer less efficient, but more concise version:
    // s2 := append([]int{}, s[2:4]...)

    for i := range s2 {
        s2[i] += 10
    }

    fmt.Println("f1", s2, len(s2), cap(s2))
}

func main() {
    s := []int{1, 2, 3, 4, 5}
    f1(s)
    fmt.Println("main", s, len(s), cap(s))
}

Output:

in function f1 [13 14] 2 3
in function main [1 2 3 4 5] 5 5

Как видим, оригинальный срзе не изменился, т.к. мы скопировали и он больше не указывает на массив первого среза.

Самым полезным свойством среза является то, что он управляет ростом массива за вас. Когда срезу необходимо превысить емкость существующего массива, Go выделит совершенно новый массив с дополнительной емкостью и переместит данные в него (точнее это сделает функция append). Но это является еще одной ловушкой, например, если вы ожидаете что два среза будут указывать на один и тот же массив.

func main() {
    // make a slice with length 3 and capacity 4
    s := make([]int, 3, 4)

    // initialize to 1,2,3
    s[0] = 1
    s[1] = 2
    s[2] = 3

    // capacity of the array is 4
    // adding one more number fits in the initial array
    s2 := append(s, 4)

    // modify the elements of the array
    // s and s2 still share the same array
    for i := range s2 {
        s2[i] += 10
    }

    fmt.Println(s, len(s), cap(s))    // [11 12 13] 3 4
    fmt.Println(s2, len(s2), cap(s2)) // [11 12 13 14] 4 4

    // this append grows the array past its capacity
    // new array must be allocated for s3
    s3 := append(s2, 5)

    // modify the elements of the array to see the result
    for i := range s3 {
        s3[i] += 10
    }

    fmt.Println(s, len(s), cap(s)) // still the old array [11 12 13] 3 4
    fmt.Println(s2, len(s2), cap(s2)) // the old array [11 12 13 14] 4 4

    // array was copied on last append [21 22 23 24 15] 5 8
    fmt.Println(s3, len(s3), cap(s3)) 
}

Еще одной приятной особенностью является то, что срезы не нужно проверять на нулевое значение и не всегда нужно инициализировать. Такие функции, как len, cap и append, отлично работают с нулевым срезом:

func main() {
    var s []int // nil slice
    fmt.Println(s, len(s), cap(s)) // [] 0 0
    s = append(s, 1)
    fmt.Println(s, len(s), cap(s)) // [1] 1 1
}

Но, надо помнить, что пустой срез - это не то же самое, что нулевой срез:

func main() {
    var s []int // this is a nil slice
    s2 := []int{} // this is an empty slice

    // looks like the same thing here:
    fmt.Println(s, len(s), cap(s)) // [] 0 0
    fmt.Println(s2, len(s2), cap(s2)) // [] 0 0

    // but s2 is actually allocated somewhere
    fmt.Printf("%p %p", s, s2) // 0x0 0x65ca90
}

Еще одна ловушка, когда вы инициализируете срез через make:

s := make([]int, 3)
s = append(s, 1)
s = append(s, 2)
s = append(s, 3)

Как вы думаете, что будет? Вот так выглядит сигнатура make для срезов:

func make([]T, len, cap) []T

Конечно, он инициализирует срез с длиной 3 и емкостью! Т.е. по факту мы сделали следующее:

s := make([]int, 3, 3)
...

И вывод кода выше будет следующим:

[0 0 0 1 2 3]

Это может привести к неожиданным ошибка, если не понимать работу срезов. Чтобы код выше вывел ожидаемые 1,2,3, нам необходимо изменить инициализацию через make след. образомм:

s := make([]int, 0, 3)
...

// [1 2 3]

Иногда встречаются варианты с индексированным доступом, что тоже в принципе верно и вполне рабочее:

s := make([]int, 3, 3)
    	
for i, v := range []int{1, 2, 3} {
	s[i] = v
}
	
fmt.Println(s)

Ну и напоследок покажу многомерные срезы, собственно это же можно проделать и с массивами:

x := 2
y := 3
s := make([][]int, y)

for i := range s {
    s[i] = make([]int, x)
}

fmt.Println(s) // [[0 0] [0 0] [0 0]]

Дата и время

time.LoadLocation читает из файла

Одна из моих любимых ловушек в Go. Для преобразования между часовыми поясами вам сначала необходимо загрузить информацию о местоположении. Оказывается, time.LoadLocation читает из файла каждый раз, когда он вызывается. Не самое лучшее решение, анпример, если нам нужно делать это при форматировании каждой строки массивного CSV-отчета:

package main

import (
    "testing"
    "time"
)

func BenchmarkLocation(b *testing.B) {
    for n := 0; n < b.N; n++ {
        loc, _ := time.LoadLocation("Asia/Kolkata")
        time.Now().In(loc)
    }
}

func BenchmarkLocation2(b *testing.B) {
    loc, _ := time.LoadLocation("Asia/Kolkata")
    for n := 0; n < b.N; n++ {
        time.Now().In(loc)
    }
}

// Output:

BenchmarkLocation-8 16810 76179 ns/op 58192 B/op 14 allocs/op
BenchmarkLocation2-8 188887110 6.97 ns/op 0 B/op 0 allocs/op