poster

Решил в свободное время изучить новый(для себя) язык программирования Golang или Go. Начал изучения с сайта Stepik, на котором имеется отличный бесплатный курс он состоит из теории и практики, ниже я представляю свой конспект этого курса, помимо этого сохраняю решения которые мне понравились, по мере изучения языка некоторые решения могут быть изменены.

Перебор числа любого разряда с конца

var x int
for x > 0 {
   fmt.Print(x % 10)
   x /= 10
}

var r int
for i:=1; x>0; x/=10 {
   c := x%10
   r += c*i
   i *= 10
}

Замена переменных местами

Например нам надо чтобы переменная А стала равна переменной Б, а Б в свою очередь стал равный А с прошлым значением до равенства с Б.

var a,b int
a,b = b,a

Формат вывода через Printf

Через функцию Printf можно выводить данные в различном виде с помощью спецификаторов, вот некоторые из часто применяемых:

  • %q - для вывода символов в одинарных кавычках
  • %в - для вывода чисел в десятичной системе
  • %t - для вывода значения типа boolean
  • %T - для вывода типа переменной
  • %f - для чисел с плавающей точкой (%9.2F) Где 9 ширина, 2 точность этим параметром можно пренебречь
  • %v - универсальный спецификатор (для строк, чисел с плавающей точкой, целочисленных и boolean)
// Вывод числа
var a int = 2
fmt.Printf("Жили-были %d веселых гуся", a)

// Вывод типа переменной
fmt.Printf("%T", a)

Функция с Return, который возвращает

Особенность языка, которая позволяет автоматически возвращать все указанные для возврата переменные без указания их в return

func sumInt(n ...int ) (c, s int) {
   for _, e := range n {
       c++
       s+=e
   }
   return
}
// Результатом выполнения будет два значения переменных С и S

Работа с env

У каждого проекта могут быть свои настройки, также проект может быть расположен на тестовой машине и нам необходимо будет указать собственные пути к компилятору для этого воспользуемся консолью

// Получаем текущие параметры env
go env

// Устанавливаем собственный путь для бинарника приложения 
go env -w GOBIN=/somewhere/else/bin

// Возвращаем путь на стандартный
go env -u GOBIN

Нюансы работы с командной строкой

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

// Мы находимся в папке hello
go install example.com/user/hello

// можно сократить до
go install .

// или аналогично
go install

Установка внешних модулей

// Импортируем внешний модуль
import "github.com/google/go-cmp/cmp"

// Загружаем все зависимости
go mod tidy 
// или
go install 

// Зависимости скачиваются по пути из конфига GOPATH/pkg/mod и путь зависимости

// Удалить все зависимости
go clean -modcache

// можно сократить до
go install .

// или аналогично
go install

Указатели

Простой пример для понимания для чего нужны указатели

& — это “адрес этого блока памяти”.

Указатели

В первом случае мы копируем все эти блоки памяти — и, в реальности, их может быть запросто больше, чем 2, хоть 2 миллиона блоков, и они все будут копироваться, а это одна из самых дорогостоящих операций. Во втором же случае, мы копируем лишь один блок памяти — в котором хранится адрес в памяти

   var a int
   var b *int = &a       // *int - получает указатель переменной А
   var c int = *b      // получает значение по указателю B
   fmt.Println(a, b, c)  //  выведет "0 0x10410020 0"
  • “Амперсанд” & звучит похоже на “Адрес” (ну, и то слово и другое на “А” начинается, по крайней мере), поэтому &x читается как “адрес переменной X”
  • звёздочка * ни на что не похоже на звучит, но может использоваться в двух случаях — перед типом (var x *MyType — означает “тип указателя на MyType”) и перед переменной (*x = y — означает “значение по адресу”)

Структуры

Структуры это что-то типо классов или ассоциативных массивов в других языках

// Если хотим определяем содержимое структуры
var People struct {
   name string
   age int
   from string
}
// Создаём экземпляр
p := People{
   name: "Nill",
   age: 35,
   from:"LA",
   }
// или
p := People{"Nill",35,"LA"}
// или пустую структуру
p := People{} //если не указывать значение полей то они примут стандартные значения для своих типов int - 0, float - 0.0, string - "" и т.д

Пример создания метода структуры

// Создаем структуру
type People struct {
   age int
   from, name string
}
// Создаем метод 
func (p *People) ageDeath() int {
   return 100 - p.age
}
// Создаем экземпляр и вызываем метод
func main() {
   p := People{32, "LA", "Nike"}
   fmt.Print(p.ageDeath())
}

Структуры также могут быть вложены друг в друга. И доступ к методам дочерних структур имеет родитель в которого они вложены

type Person struct {
   name string
}
func (p *Person) SeyWelcome() {
   fmt.Print("Hi, my name is ",p.name)
}

type Boss struct {
   About Person
   Model string
}
// или если имя структуры не меняется можно сократить
type Boss struct {
   Person
   Model string
}

p := new(Boss)
p.Person.SeyWelcome()

// т.к структура Person вложена в Boss мы можем вызвать её метод короче
p.SeyWelcome

Строки

Go содержит различные функции для работы со строками в пакете string. Вот некоторые из них

//Cчитываем из терминала
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
input := scanner.Text()

//Содержится ли подстрока в строке
strings.Contains("ivan","an") // true

//Заменяет любое old на new n - раз, если n = -1 то заменены будут все вхождения func Replace(s, old, new sring, n int) string
strings.Replace("Голосуй", "о", "а", -1) // Галасуй

// Вырезает из строки подстроку
strings.Trim("довод","од") //в

// Вырезает из строки все кроме чисел
a := "12312512fsfsafsaqweqw5"
a = strings.TrimFunc(a, func(r rune) bool {
   return !unicode.IsNumber(r)
})
// a = "123125125"

Важно помнить что функция len() возвращает количество байт а не кол-во символов строки, для получения количества символов следует использовать библиотеку utf8 и функцию RuneCounInString()

var ru = "время ток"
fmt.Print(utf8.RuneCountInString(ru)) //9

Считать строку с пробелами

s := bufio.NewReader(os.Stdin).ReadString('\n')

В целом работа со строками достаточно сложная в отличии от того же php, к примеру чтобы проверить строку соответствует ли она условиям (1 символ с большой буквы, последний символ с точкой) я написал полотно, но нашлось решение покороче

   // bufio используется для считывания из консоли строки с пробелом
   text, _ := bufio.NewReader(os.Stdin).ReadString('\n')
   // в зависимости от консоли к строке могут быть добавлены символы поэтому избавляемся от них если они есть
   text = strings.Trim(text, "\n\r")
   runes := []rune(text)
   if unicode.IsUpper(runes[0]) && runes[len(runes)-1] == '.' {
       fmt.Println("Yes")
   } else {
       fmt.Println("NO")
   }

Преобразование типов

числа в строку

var a int = 102
b := strconv.Itoa(a)
fmt.Print("%T", b) //string

var a float64 = 1.0123456789
   // 1 параметр - число для конвертации
   // fmt - форматирование
   // prec - точность (кол-во знаков после запятой)
   // bitSize - 32 или 64 (32 для float32, 64 для float64)
   fmt.Println(strconv.FormatFloat(a, 'f', 2, 64)) // 1.01

строка в число

a := "10"
b := "15"
c, err := strconv.Atoi(a) 
// Важно читать ошибку т.к строка может не содержать чисел!

// float
s := "23.23456"
   // здесь 2 параметр - bitSize
   // bitSize - 32 или 64 (32 для float32, 64 для float64)
   // но нужно понимать что метод все равно вернет float64
result, err := strconv.ParseFloat(s, 64)

Функции

Стоит помнить, что язык Golang позволяет пропускать возвращаемые значения

// Например функция возвращает число int  и ошибку, и ошибку мы проверили раньше то можно получить только число
result,_ := minusCount(a,b)
// В данном случае мы результат записываем в result, а возвращаемую ошибку опускаем 

оператор defer

Он позволяет выполнить определенную операцию даже если сработает panic (он прекращает работу программы ), если будет объявлено несколько операторов defer то они сработают в последовательности LIFO (Last-In, First-Out) т.е тот кто был определен первым сработает ПОСЛЕДНИМ. Также стоит помнить что значения переменных которые использует данный оператора сохраняются на момент его определения и они могут быть неактуальны

a := 5
defer myTest(a) // a=5
a = 7

Отображения (Map)

На первых взгляд это что-то типо ассоциативных массивов, пример вызова

m1 := make(map[int]int)
m2 := map[int]int{12:2,}  // Ключ значения можно не указывать

//Удаление элемента
delete(m2,12) // получим map[]

Проверка на наличие ключа в map

brandsMap := map[string]string{
   "bmw": bmw,
   "audi": bmw,
   "lada": nil
}
_, value := brandsMap["lada"]
fmt.Print(value) // true,  
//value - выдаст true или false в зависимости от того имеется ли ключ lada в brandsMap

Важно помнить, что если перебирать map используя range то, каждый раз результат последовательности вывода данных будет разным, поэтому чтобы контролировать выдачу надо использовать массив ключей мап и отдельного его перебирать

Интерфейсы

Интерфейсы есть во многих языках, в го интерфейсы указывают на необходимость описания всех методов которые необходимы какому либо объекту

Также неплохая статья на хабре

Приведение типа

var i interface{} = 12

if v, ok := i.(int); ok {
   fmt.Println(v+12) // Суммирование не произойдет, если ok == false
}

switch v := i.(type) {
   case T1:
       ...
   case T2, T3:
       ...
   default:
       ...
}

Работа с файлами

Для работы с файлами рекомендуется использовать библиотеку os

file, err := os.ReadFile("data.json")
if err != nil {
   fmt.Print("Ошибка ", err)
} else {
   fmt.Print("Файл получен!")
}

Работа с json

Для работы с данным форматом есть библиотека json, которая имеет кодировать Marshal и декодировать Unmarshal файл. Для декодирования и кодирования необходимо создать тип данных структуру которая будет (частично) соответствовать в случае с декодированием и полностью если кодировать файлу json. Ниже рассмотрим пример декодирования

type Animals struct {
    Cats []struct {
        name string
        age int
    }
}

var animal *Animals
err = json.Unmarshal(file, %alimal)
// Все записывается в переменную структуры которую можно потом перебрать

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

Дата и время

//Получаем текущую дату и время
now := time.Now() //2021-08-05 19:23:21.516443297 +0400 +04 m=+0.000080777

// Форматируем вывод даты
fmt.Println(now.Format("02-01-2006 15:04:05"))

// парсит дату и время в строковом представлении
firstTime, err := time.Parse("2006/01/02 15-04", "2020/05/15 17-45")
if err != nil {
	panic(err)
}

firstTime := time.Date(2020, time.May, 15, 17, 45, 12, 0, time.Local)
secondTime := time.Date(2020, time.May, 15, 16, 45, 12, 0, time.Local)

// func (t Time) After(u Time) bool
// true если позже
fmt.Println(firstTime.After(secondTime)) // true

// func (t Time) Before(u Time) bool
// true если раньше
fmt.Println(firstTime.Before(secondTime)) // false

// func (t Time) Equal(u Time) bool
// true если равны
fmt.Println(firstTime.Equal(secondTime)) // false

now := time.Date(2020, time.May, 15, 17, 45, 12, 0, time.Local)

// func (t Time) Add(d Duration) Time
// изменяет дату в соответствии с параметром - "продолжительностью"
future := now.Add(time.Hour * 12) // перемещаемся на 12 часов вперед

// func (t Time) AddDate(years int, months int, days int) Time
// изменяет дату в соответствии с параметрами - количеством лет, месяцев и дней
past := now.AddDate(-1, -2, -3) // перемещаемся на 1 год, два месяца и 3 дня назад

// func (t Time) Sub(u Time) Duration
// вычисляет время, прошедшее между двумя датами
fmt.Println(future.Sub(past)) // 10332h0m0s

Но лучше рассмотреть библиотеку fmtdate

date := fmtdate.Format("DD.MM.YYYY", time.Now())

var err
date, err = fmtdate.Parse("M/D/YY", "2/3/07")


Параллелизм

в языке го используется горотина для создания параллельного выполнения кода

Горутина

func hello() {
     for {
    	fmt.Println("hello")
     }
}
func bb() {
    fmt.Println("bb men")
}

func main() {
    hello()
    bb()
}
// Если запустить такой код то программа будет в вечном цикле выводить в консоль hello, но если мы добавил горутину, мы будет получать иногда и результат функции bb

func main() {
    go hello()
    go bb()
    time.Sleep(1 * time.Second)
}
// hello hello bb men hello .... приблизительно такой ответ

Каналы

Хорошая статья на хабре

Канал — это объект связи, с помощью которого горутины обмениваются данными. Технически это конвейер (или труба), откуда можно считывать или помещать данные.

В go каналы являются указателями.

Канал имеет две основные операции:

  1. Отправление (запись) - передает через канал значения из одной горутины в другую
  2. Получение (чтение) - получение через канал значения из другой горутины
channel := make(chan int) //создаем канал channel с типом данных int
channel <- num // отправляет в канал channel значение num
num = <- channel // получение из канала channel в переменную num
<- channel // извлекаем данные и никуда не записываем
//Чтение из канала можно производить в цикле
for v := range channel {
    ...
}

// Закрываем канал
close(channel)

Чтение из канала будет осуществляться пока канал открыт.

Помимо типов данных канал имеет длину и ёмкость (их можно получить через len() и cap():

  • длинна - количество значений в канале в текущий момент
  • емкость - размер буфера
c := make(chan int, 1) // здесь 1 - размер буфера
fmt.Println(len(c), cap(c)) // 0 1
c <- 1
fmt.Println(len(c), cap(c)) // 1 1
<-c

Если при создании канала указывается ёмкость = 0 или не указывается совсем, то такой канал считается не буферизированным.

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

Deadlock (Взаимная блокировка)

Чтение или запись данных в канал блокирует горутину и контроль передается свободной горутине. Если такие горутины отсутствуют либо они все “спят” в этот момент может возникнут deadlock который приведет к завершению программы.

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

// Проверка закрыт ли канал
val, ok := <- channel
if ok == false {
    fmt.Print("Канал закрыт")
} else {
    fmt.Print("Канал открыт")
}

// Если использовать range то автоматически завершится цикл когда канал будет закрыт
for val := range channel {
    fmt.Print(val)
}

runtime.NumGoroutine() // можно вывести номер goroutine обычно 1 это main

Горутина не блокируется до тех пор пока буфер не будет заполнен, но при чтении из канала операция не будет завершена пока не опустошит весь буфер!

Однонаправленные каналы

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

roc := make(<-chan int) // для чтения
soc := make(chan<- int) // для записи

Прочее

  • Если в пакете функция начинается с ЗАГЛАВНОЙ буквы то она может экспортироваться в другой пакет, который импортирует наш пакет
//пример получения всей строки без учетов пробелов
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
s := scanner.Text()

Проверка числа на чётность

var i int
i = 3
fmt.Print(i&1 == 0) // False 

Источники

  1. habr
  2. go.dev
  3. stepik.org