Go — это высокоуровневый язык программирования с открытым исходным кодом, на котором можно создавать надежные и при этом простые программы.
Go разрабатывался в компании Google с 2007-го года и уже в 2009-м году был представлен официально. Это выразительный, компактный язык с понятным синтаксисом, что упрощает создание программ.
Благодаря механизмам параллелизма этот язык позволяет пользоваться преимуществами многоядерных и сетевых компьютеров.
Несмотря на то, что это компилируемый язык со статической типизацией, с ним так же легко работать, как и с интерпретируемым языком с динамической типизацией. Go поддерживают ОС Linux, Windows, macOS, OpenBSD, DragonFly BSD, FreeBSD, NetBSD, Solaris.
Синтаксис Go напоминает синтаксис C. Некоторые элементы заимствованы из языка Оберон и скриптовых языков. Это регистрозависимый язык. В идентификаторах и строках полностью поддерживается Юникод.
Чтобы получить базовое представление о синтаксисе Go, разберем простую программу:
package main
import "fmt"
// The entry point
func main() {
fmt.Println("Привет, Go!")
}
Вначале необходимо определить имя пакета. Это делается с помощью ключевого слова package:
package main
Программы на Go состоят из пакетов. Пакет — это набор исходных файлов, которые находятся в одном и том же каталоге и компилируются вместе.
Функции, типы, переменные и константы, определенные в одном исходном файле пакета, доступны во всех остальных исходных файлах этого пакета.
Пакет main используется для создания исполняемого двоичного файла. Выполнение программы начинается с вызова функции main из этого пакета.
Один пакет можно импортировать в другой. Для этого используется ключевое слово import. За ним указывается имя импортируемого пакета в кавычках:
import "fmt"
Следующая строка содержит комментарий. В Go используются комментарии в стиле C. В этом примере видим строчный комментарий, который начинается с двух косых черт (// ...). Блочные комментарии из нескольких строк вставляются между /* и */. Компилятор воспринимает строчный комментарий и блочный комментарий из нескольких строк как перевод строки, а однострочный блочный — как пробел.
Определение функции main:
func main() {
fmt.Println("Привет, Go!")
}
Оно начинается с ключевого слова func, за которым следует имя функции, список аргументов в скобках (скобки ставятся и при отсутствии аргументов) и открывающая фигурная скобка. В следующих строках располагается код тела функции, за которым в отдельной строке ставится закрывающая фигурная скобка.
Стиль напоминает С, но есть некоторые важные отличия. Обратите внимание на положение открывающей фигурной скобки и на отсутствие точки с запятой после единственной строки в теле функции.
В отличие от языка C, точка с запятой в Go используется редко: в некоторых случаях в операторах if, for и switch, а также для разделения команд, помещенных в одной строке. Компилятор сам расставляет точки с запятой в конце строк, поэтому нужно запомнить, что перенос строки допустим не в любом месте, где можно использовать пробел. Например, в объявлениях функций необходимо ставить фигурную скобку после круглой, завершающей список аргументов:
func a(){
}
Если бы фигурная скобка располагалась на следующей строке, то компилятор вставил бы запятую после закрывающей круглой скобки:
func a();
{
}
По той же причине ключевое слово else следует располагать в одной строке с закрывающей фигурной скобкой после if, иначе точка с запятой появилась бы после этой скобки:
if condition {
...
};
else {
...
}
Код тела функции состоит из единственной строки, которая выводит строку «Привет, Go!» и переводит курсор на следующую строку:
fmt.Println("Привет, Go!")
В этой строке мы вызываем функцию Println из пакета fmt. Для этого сначала указываем имя пакета, затем через точку — имя функции, а в скобках приводим строковой аргумент в прямых двойных кавычках. Двоеточие после вызова функции не ставим, потому что его подставит компилятор.
Сохраните код из начала раздела в файле hello.go и запустите программу в командной строке или терминале:
go run hello.go
После недолгой компиляции будет выведен результат:
Теперь расширим возможности этой маленькой программы: научим ее «обращаться» к пользователю по имени.
Ознакомьтесь с кодом:
package main
import (
"fmt"
"os"
)
func main() {
var name string
fmt.Print("Как тебя зовут? ")
fmt.Fscan(os.Stdin, &name)
fmt.Println("Привет,", name + "!")
}
Чтобы ввести имя пользователя с консоли, воспользуемся функцией Fscan из пакета os. Для этого в текущий код нужно добавить еще одно объявление импорта. Можно вставить еще одну строку:
import "fmt" import "os"
Такой вид импорта называется прямым. Но рекомендуется использовать более удобный способ — групповой:
import (
"fmt"
"os"
)
При его использовании код выглядит понятнее и не приходится каждый раз указывать ключевое слово import.
В первой строке тела функции объявляем переменную строкового типа. Для этого используем ключевое слово var, после него через разделитель укажем имя переменной, после чего определим ее тип (тоже через разделитель):
var name string
По умолчанию строковая переменная получает значение пустой строки. Если нужно инициализировать переменную, ее значение можно указать в объявлении:
var name string = "No name"
Далее выведем запрос на ввод имени:
fmt.Print("Как тебя зовут? ")
Для вывода используется команда Print, которая работает так же, как и Println, но не переводит строку. После запроса в строке оставлен пробел, чтобы отделить запрос от ввода.
Далее считаем ввод в переменную name из стандартного ввода os.Stdin:
fmt.Fscan(os.Stdin, &name)
Теперь по адресу (&) переменной name находится введенный текст.
Выведем приветствие на экран:
fmt.Println("Привет,", name + "!")
Если в функции Println указано несколько аргументов, то они выводятся через пробел, поэтому перед закрывающей кавычкой здесь пробел не ставим. Второй аргумент получаем с помощью конкатенации — «сложения» строк.
Вывод будет выглядеть так:
Пришло время подробнее рассмотреть использованные в примерах возможности языка.
В Go используется несколько типов импорта.
| Типы импорта в Go | ||
| Пример | Описание | Использование в коде |
import "fmt" | Прямой импорт. В кавычках указывается путь к пакету. Для встроенных пакетов указывается только их имена. Также можно указывать пути в интернете, например: github.com/<username>/<package> | fmt.Print("Go") |
import (
"fmt"
"os"
)
| Групповой импорт. | os.Stdin |
import "math/rand" | Вложенный импорт. Используется, когда нужно воспользоваться только подмножеством функций пакета. | rand.Int(100) |
import f "fmt" | Импорт с псевдонимом | f.Println("Go") |
import . "math" | Импорт без указания имени пакета (квалификации). Используется, в основном, для тестирования публичных элементов. Может вызвать конфликт пространства имен. | Sin(1.0) |
import _ "math" | Пустой импорт. Если пакет импортирован, но не используется, то в Go возникает ошибка. Чтобы она не возникала, используется пустой импорт. Когда потребуется воспользоваться пакетом, определение импорта можно будет заменить другим. | |
Мы уже упоминали выше, что в Go используется статическая типизация. Тип переменной определяется в объявлении и его невозможно изменить впоследствии. Это позволяет избежать некоторых распространенных ошибок.
Рассмотрим встроенные типы данных.
Числовые типы
| Целые числа | |
| Тип | Описание |
| uint8 и byteuint16 uint32 uint64 | Беззнаковые целые числа. Принимают только неотрицательные значения. 8, 16, 32 и 64 — это количество бит, которое отведено на число соответствующего типа. |
| int8 int16 int32 и rune int64 | Знаковые целые числа. |
| uint int uintptr | Машинно-зависимые целочисленные типы. |
| Вещественные числа | |
| Тип | Описание |
| float32 | С одинарной точностью |
| float64 | С двойной точностью |
Еще существуют значения NaN (not a number) для результатов таких вычислений, как 0/0, а также положительная и отрицательная бесконечность (+∞ и −∞).
| Комплексные числа | |
| Тип | Описание |
| complex64 | Вещественная и мнимая части представлены числами с типом float32 |
| complex128 | Вещественная и мнимая части представлены числами с типом float64 |
Над числами в Go можно производить приведенные ниже операции.
| Операции над числами в Go | |
| + | сложение |
| – | вычитание |
| * | умножение |
| / | деление |
| % | остаток от деления |
Важно знать, что Go не поддерживает неявное приведение типов. Поэтому операции можно проводить только над данными одного и того же типа.
Строки
Строки в Go представляют собой неизменяемые последовательности символов UTF-8. Они заключаются в двойные кавычки или в обратные галочки.
В строках в двойных кавычках можно использовать управляющие символы, такие как \r, \t.
Строки в обратных галочках являются «сырыми» литералами. Они не поддерживают управляющие символы, но могут состоять из нескольких строк и содержать любые символы, кроме обратной галочки. Обычно они используются для многострочных сообщений, в регулярных выражениях и HTML.
Строки могут быть пустыми, но пустые строки не являются нулевыми.
Для строк предусмотрена операция конкатенации, как мы уже видели во втором примере кода. Для нее используется тот же оператор, что и для сложения (+).
Чтобы узнать количество символов в строке, независимо от кодировки, используется функция RuneCountInString() из пакета UTF-8. Функция len()возвращает число байтов в строке.
Важно помнить, что строки неизменяемы, и попытка изменить строку приводит к ошибке.
Булевский тип
Это однобайтовый целочисленный тип, значения которого представляют истинность и ложность (соответственно true и false).
Для этого типа предусмотрены три оператора:
| Логические операторы | |
| && | и |
| || | или |
| ! | не |
В Go поддерживаются также массивы, представляющие собой последовательности элементов одного типа.
Объявление переменной начинается с ключевого слова var, после разделителя указывается имя переменной, а за ним после разделителя — тип:
var x int32
После объявления переменных разных типов они получают нулевые значения: все числовые типы — 0, строковые — пустую строку, указатели — nil.
После объявления переменной можно присвоить ей значение. Для явного присваивания значений переменной с заданным типом используется символ =:
x = 100
Присвоить переменной значение можно и в объявлении. Инициализация переменной с указанием типа производится так:
var y string = "Hello"
Поскольку в Go поддерживается автоматический вывод типов, при инициализации можно не указывать тип переменной:
var z = true
Локальные переменные можно объявлять и инициализировать с помощью сокращенной формы записи и оператора := :
n = 20
Если нужно объявить несколько переменных, можно заключить их объявления в скобки:
var (
firstName string = "Luke"
lastName string = "Skywalker"
)
При объявлении массива перед типом в квадратных скобках указывается количество элементов:
var numbers [5]int
Константы в Go бывают типизированные и нетипизированные. Типизированные константы объявляются так же, как и переменные с инициализацией, только с использованием ключевого слова const:
const pi float64 = 3.1415
Нетипизированная константа определяется без указания типа:
const a = 1
После этого объявления значение 1 не отнесено к какому-либо типу. Это просто целочисленное значение. Оно может использоваться в операциях с целыми числами.
Условные конструкции
Конструкция if…elseпринимает выражение, которое возвращает булевское значение. Если возвращено значение true, то выполняется блок кода в фигурных скобках, следующий за конструкцией:
if x > 0 {
fmt.Println("x - положительное")
}
Если в приведенном выше примере кода значение x окажется больше нуля, то в консоли будет выведена строка. В противном случае управление будет передано коду, следующему за закрывающей фигурной скобкой.
Если нужно, чтобы один код выполнялся при истинном значении условного выражения, а другой — при ложном, используется ключевое слово else:
if x > 0 {
fmt.Println("x - положительное")
} else {
fmt.Println("x - не положительное")
}
Условия можно «нанизывать»:
if x > 0 {
fmt.Println("x - положительное")
} else if x < 0 {
fmt.Println("x - отрицательное")
} else {
fmt.Println("x - ноль")
}
Если требуется сравнить некоторое выражение с набором значений и в каждом случае выполнить соответствующие действия, используется конструкция switch:
switch(n) {
case 1:
fmt.Println("n = 1")
case 2:
fmt.Println("n = 2")
case 3, 4: // несколько значений
fmt.Println("n = 3 или 4")
default: // если не совпало с указанными значениями
fmt.Println("значение n не соответствует предусмотренным")
Циклы
В Go есть только один цикл — for.Он имеет следующую структуру:
for [инициализация;] [условие;] [приращение] {
// действия
}
Например:
for i = 1; i < 10; i++ {
fmt.Println(i * i)
}
В этом цикле выводятся квадраты чисел от 1 до 9. Вначале счетчик получает значение 1 и при каждой итерации оно увеличивается на единицу. Итерации прекращаются по достижении счетчиком значения 10.
Необязательно указывать все условия. При этом нужно предусмотреть инициализацию и приращение значения счетчика, а также условие выхода из цикла:
i = 0
for{
if i < 5 {
continue
} else if i > 9 {
break
}
fmt.Println(i * i)
}
Этот цикл выведет квадраты чисел от 5 до 9 включительно. Пока значение счетчика будет меньше 5, ключевое слово continue не будет передавать управление строке, которая выводит значение на экран. Начиная со значения 5 значение будет выводиться, а когда счетчик получит значение 10, ключевое слово break осуществит выход из цикла.
Объявление функции в Go:
func имя (параметры) (типы_возвращаемых_значений){
операторы
}
Функция, не возвращающая значение:
func hello() {
fmt.Println("Hello!")
}
Для возврата значения из функции используется оператор return и указывается тип возвращаемого значения:
func add (a int, b int) int {
return a + b
}
Возвращаемое значение может быть именованным:
func add (a int, b int) (z int) {
z = a + b
return
}
Поскольку переменная z определена как возвращаемое значение, ее не приходится указывать после оператора return.
Функция может возвращать несколько значений:
func addmul (a, b int) (int, int) {
c = a + b
d = a * b
return c, d
}
x, y = addmul(2, 5)
Эти результаты также могут быть именованными:
func addmul (a, b int) (c, d int) {
c = a + b
d = a * b
return
}
Поскольку в Go не только пакеты, но и объявленные переменные обязательно должны использоваться в программе, предусмотрена псевдопеременная _. Она позволяет проигнорировать возвращаемое значение.
Например, если нужно получить только результат сложения, следующий код выдаст ошибку:
x = addmul(2, 5)
Псевдопеременная позволяет избежать ошибки:
x, _ = addmul(2, 5)
В Golang не реализованы обработчики исключительных ситуаций. Вместо них и блоков с гарантированным завершением используется ключевое слово defer. Указанная после него функция будет выполнена по завершении функции, в которой используется эта конструкция:
func main() {
defer deferred()
fmt.Println("Программа запущена")
}
func deferred(){
fmt.Println("Выполнение программы завершено")
}
Несмотря на то, что функция, выводящая строку «Программа запущена», находится в конце функции main, она будет вызвана первой из двух строк тела функции.
Оператор panic используется для генерирования ошибки и выхода из программы. Ему можно передать строку, которая будет выведена в консоли. Кроме этой строки выводится диагностическая информация. Перед завершением программы выполняются все функции, вызванные ключевым словом defer.
Выполним такую программу:
package main
import "fmt"
func main () {
defer bye()
fmt.Println(div(8, 2))
fmt.Println(div(8, 0))
}
func div (a, b float32) float32 {
if b == 0 {
panic("Попытка деления на ноль.")
}
return a/b
}
func bye () {
fmt.Println("Пока!")
}
В результате ее выполнения в консоли будет выведено число 4, строка «Пока!» и сообщение об ошибке:
Поток программы создается ключевым словом go. Оно запускает функцию в новой сопрограмме. В Go такие сопрограммы называются go-процедурами. Они выполняются параллельно. При использовании многоядерных процессоров эти процедуры могут выполняться на разных ядрах, что ускоряет работу программы.
Go-процедуры экономны по сравнению с нитями. Размер их стека составляет лишь несколько килобайт, и он может изменяться в зависимости от требований приложения.
Рассмотрим простой пример использования go-процедур:
package main
import (
"fmt"
)
func hello() {
fmt.Println("Hello from goroutine")
}
func main() {
go hello()
fmt.Println("Hello from main function")
}
Программа создает go-процедуру, которая может вывести в консоли приветствие Hello from goroutine, затем выводит приветствие Hello from main function. Запустим программу в консоли несколько раз:
Как видим, результаты разные. Иногда первой выводится строка из go-процедуры, иногда — из функции main, а при последнем вызове строка из go-процедуры не выводится вовсе. Дело в том, что когда завершается выполнение функции main, завершается и работа всей программы, в том числе всех go-процедур. После вызова go-процедуры вызвавшая ее функция продолжает работу. Если go-процедура успевает выполнить свою работу перед завершением работы программы, выводится соответствующая строка. Если же нет — эта строка не выводится.
Чтобы сделать результаты более предсказуемыми, добавим перед строкой вывода в функции main еще одну строку, в которой будет считываться ввод из консоли:
package main
import (
"fmt"
)
func hello() {
fmt.Println("Hello from goroutine")
}
func main() {
go hello()
fmt.Scanln()
fmt.Println("Hello from main function")
}
В результате получим:
Go-процедуры обмениваются данными через каналы. Для их создания используется ключевое слово chan с указанием типа элемента. Переменная для канала, по которому передаются целые числа, определяется таким образом:
var intChannel chan int
Теперь по нему можно передавать числа с помощью оператора <- :
intChannel <- 7 // передаем данные в канал intValue := <- intChannel // получаем данные из канала
После определения канала приведенным выше способом он не инициализирован и имеет значение nil. Чтобы его инициализировать, нужно воспользоваться функцией make().
Каналы бывают буферизированные и небуферизированные. Для создания небуферизированного канала используется функция make() без указания аргументов (емкости канала):
var intChannel chan int = make(chan int)
Также можно воспользоваться выводом типов:
intChannel := make(chan int)
Процедура-получатель работает только тогда, когда в канале есть данные. Когда канал пуст, ее работа блокируется. Процедура-отправитель передает данные в канал, когда он пуст. После отправки данных ее работа блокируется до получения данных из канала. Чтобы продемонстрировать работу с каналами, запустим следующий код:
package main
import "fmt"
func main() {
intChannel := make(chan int)
fmt.Println("Функция main запускает Go-процедуру.")
go func(){
fmt.Println("Go-процедура отправляет данные в канал.")
fmt.Println("Ее работа блокируется.")
intChannel <- 10
fmt.Println("Данные переданы в канал.")
fmt.Println("Go-процедура разблокирована.")
}() // В качестве go-процедур можно использовать замыкания
intResult := <-intChannel
fmt.Println("Функция main получила данные из канала:", intResult)
}
Вывод программы будет выглядеть так:
В этой статье дано только базовое представление о синтаксисе языка Go и рассмотрены некоторые его особенности. Подробнее о языке можно узнать из документации.
Рассмотрим примеры кода, в которых используются конструкции языка Go, с которыми вы познакомились выше.
В первом примере будем использовать функции из нескольких модулей, условные конструкции, цикл, два вида присваивания, псевдопеременную. Код не требует отдельных пояснений — читайте комментарии:
/* Основы синтаксиса Go
на примере программы
для угадывания числа */
// Объявление пакета
package main
// Импорт модулей, необходимых для работы программы
import (
"fmt"
"os"
"math/rand"
"time"
)
// Точка входа (отсюда начинается выполнение программы)
func main() {
// Переменная для загаданного числа
var secret int
// Переменная для номера попытки
var attempt int = 0
// Флаг завершения игры
var isOver bool = false
// Число пользователя
var number int = 0
var win bool = false
// Получим порождающий элемент с автоматическим выводом типа (:=)
seed := time.Now().UnixNano()
// Зададим порождающий элемент
rand.Seed(seed)
// Получим случайное число и присвоим его переменной явно (=)
secret = rand.Intn(1024) + 1
// Выведем приглашение
fmt.Println("Отгадайте целое число от 1 до 1024.")
fmt.Println("Для выхода введите 0.")
fmt.Println()
// Начнем бесконечный цикл с выходом при условии истинности isOver
for {
attempt++
fmt.Print("Введите число: ")
_, err := fmt.Fscan(os.Stdin, &number)
// Проверим, введено ли допустимое число
if err != nil {
fmt.Println("Вводите только целые числа!")
attempt--
continue
}
// Проанализируем ввод
if number == 0 { // Команда выхода?
isOver = true
} else if number == secret { // Число угадано?
win = true
isOver = true
} else if number < secret { // Догадка меньше загаданного?
fmt.Println("Маловато будет!")
} else { // Значит, догадка больше загаданного
fmt.Println("Многовато будет!")
}
if isOver { // Постусловие выхода
break
}
} // Флаг выхода установлен в true
// Выведем сообщение о результате
if win {
fmt.Println("Вы угадали число за", attempt, "попыток.")
}
fmt.Println("До свидания!")
}
Результат выполнения выглядит примерно так:
Во втором примере продемонстрируем использование go-процедуры и каналов. Программа запросит, на сколько секунд запустить таймер, и будет вести обратный отсчет, каждую секунду выводя оставшееся время, пока не сработает таймер:
package main
import (
"fmt"
"os"
"time"
)
func main() {
seconds := 0
fmt.Print("На сколько секунд запустить таймер? ")
fmt.Fscan(os.Stdin, &seconds)
counter := seconds
timer := time.NewTimer(time.Duration(seconds) * time.Second)
ticker := time.NewTicker(time.Second)
fmt.Println(counter)
go func(){
for {
select {
case <- ticker.C:
counter--
fmt.Println(counter)
case <- timer.C:
return
}
}
}()
time.Sleep(time.Duration(seconds) * time.Second)
fmt.Println("Время вышло")
}
В целом язык Go используется благодаря тому, что он позволяет повысить эффективность приложений, обеспечить многопоточную обработку данных, сократить затраты времени и усилий на разработку приложений.
Несмотря на растущую популярность Go, он не идеален, как исключительная ситуация любой другой язык программирования:
Язык Go предназначен для создания крупных распределенных проектов, которые работают на многоядерных процессорах. Программы на нем выполняются без использования виртуальной машины и быстро компилируются, обеспечивая интерактивную разработку.
Поскольку это компилируемый язык, многие компании переписывают на нем свое программное обеспечение. Это делается, чтобы ускорить обработку информации, в том числе за счет параллельных процессов.
В ситуациях, когда скорость разработки и компактность кода важнее, чем скорость выполнения программ, лучше использовать более удобный с этой точки зрения язык, например Python.
Тем не менее, Go популярен в таких сферах, как бэкенд-разработка, веб-разработка, базы данных, финансовые услуги, медиа, а чаще всего он применяется в технологическом секторе. Этот язык продолжает развиваться, и ожидается выпуск его второй версии. К тому же, он прост в изучении. Поэтому знакомство с ним не займет много времени и будет полезным. Удачи!
На фоне роста спроса на ликвидность в бычьем рынке 2025 года, криптозаймы снова выходят на…
Прокси (proxy), или прокси-сервер — это программа-посредник, которая обеспечивает соединение между пользователем и интернет-ресурсом. Принцип…
Согласитесь, было бы неплохо соединить в одно сайт и приложение для смартфона. Если вы еще…
Повсеместное распространение смартфонов привело к огромному спросу на мобильные игры и приложения. Миллиарды пользователей гаджетов…
В перечне популярных чат-ботов с искусственным интеллектом Google Bard (Gemini) еще не пользуется такой популярностью…
Скрипт (англ. — сценарий), — это небольшая программа, как правило, для веб-интерфейса, выполняющая определенную задачу.…