Краткое введение в язык программирования Go
Go — это язык программирования с открытым исходным кодом, который упрощает создание простого, надежного и эффективного программного обеспечения. (из golang.org)
Истоки го
Go был задуман в сентябре 2007 года Робертом Гриземером, Робом Пайком и Кеном Томпсоном из Google и анонсирован в ноябре 2009 года. Цель языка и сопутствующих инструментов заключалась в том, чтобы быть выразительными, эффективными как при компиляции, так и при выполнении, а также эффективен при написании надежных и надежных программ. Короче говоря, предполагается, что Go будет прагматичным.
Go имеет внешнее сходство с C, но это гораздо больше, чем обновленная версия C. Он заимствует и адаптирует хорошие идеи из многих других языков, избегая при этом функций, которые привели к сложному и ненадежному коду. Его средства параллелизма являются новыми и эффективными, а его подход к абстракции данных и объектно-ориентированному программированию отличается гибкостью. Он имеет автоматическое управление памятью или сборку мусора.
Благодаря прагматичной философии Go за последние несколько лет популярность языка программирования Go резко возросла. Согласно рейтингу IEEE Spectrum Лучшие языки программирования 2017 года, Go занимает 9-е место среди лучших языков программирования (10-е место в 2016 году, 13-е место в 2015 году).
Различные рейтинговые веб-сайты демонстрируют аналогичную восходящую тенденцию для Go. Go также находит прочную основу в области новой технологии блокчейна. Возьмем, к примеру, протокол Ethereum. Go Ethereum — одна из трех оригинальных реализаций (наряду с C++ и Python) протокола Ethereum. Go Ethereum написан на Go и имеет полностью открытый исходный код. См. Go Ethereum Github для кода, если вы заинтересованы.
Перейти просто. Любой разработчик с фундаментальным пониманием любого другого языка, такого как Java, C, C++ или JavaScript, легко поймет синтаксис и логику Go. Это тоже увлекательный процесс. Как всегда, лучший способ учиться — на примерах. Я поделюсь с вами некоторыми примерами кода и представлю основные строительные блоки Go. Я надеюсь, что после прочтения этой статьи у вас будет базовое представление о Go.
Установка Go
Инструкции по установке Go см. в разделе Начало работы. После того, как вы успешно загрузили и установили Go, введите go в командной строке, вы должны увидеть вводную информацию о Go. Чтобы лучше управлять пакетами Go и исходным кодом, обычно вам нужно установить GOPATH в указанный каталог (обычно $HOME/go). Исходный код обычно помещается в GOPATH/src. Инструкции по настройке GOPATH см. в разделе Настройка GOPATH. Предполагая, что мы установили GOPATH как $HOME/go, давайте напишем наш первый Go.
Привет мир!
package main import ( "fmt" ) func main() { fmt.Println("Hello, World!") }
Внутри $HOME/go/src мы создали новый каталог hello, а внутри каталога hello мы создали файл hello.go. Приведенный выше фрагмент кода — это то, что находится внутри hello.go. Кстати, все файлы Go заканчиваются на .go. В терминале перейдите в каталог $HOME/go/src/hello и введите следующие команды:
go build hello.go ./hello
go build hello.go создаст исполняемый файл с именем hello. Затем, если вы запустите файл hello, вы должны увидеть «Hello, World!» печатается на терминале. Вы также можете запустить следующий однострочный код, чтобы увидеть «Hello, World!»:
go run hello.go
go run hello.go создает исполняемый файл hello и тут же запускает его.
Теперь давайте объясним hello.go построчно:
package main
В Go исходные файлы организованы в системные каталоги, называемые пакетами, которые обеспечивают повторное использование кода в приложениях Go. В пределах одного каталога имя пакета будет одинаковым для всех исходных файлов, принадлежащих этому каталогу. Файл исходного кода может напрямую вызывать функции, определенные в другом файле исходного кода, если эти два файла принадлежат одному и тому же пакету.
Когда вы создаете повторно используемые фрагменты кода, вы разрабатываете пакет как разделяемую библиотеку. Но когда вы разрабатываете исполняемые программы, вы будете использовать пакет main. Пакет «main» сообщает компилятору Go, что пакет должен компилироваться как исполняемая программа, а не как разделяемая библиотека. Основная функция в пакете «main» будет точкой входа нашей исполняемой программы. Когда вы создаете разделяемые библиотеки, у вас не будет основного пакета и основной функции в пакете.
import ( "fmt" )
Go поставляется с обширной стандартной библиотекой полезных пакетов, и сообщество Go создало и поделилось многими другими. Пакет fmt — это стандартный пакет Go, который реализует форматированный ввод-вывод с функциями, аналогичными printf и scanf языка C. «Глаголы» формата происходят от C, но они проще. Чтобы использовать пакет fmt, мы должны сначала его импортировать. Довольно часто нам нужно импортировать несколько пакетов, вот типичный пример:
import ( "fmt" "io" "net/http" "os" "strings" )
Давайте перейдем к функции main() в hello.go.
func main() { fmt.Println("Hello, World!") }
В Go ключевое слово func определяет функцию. В вашем приложении Go может быть только одна основная функция. Функция fmt.Println() печатает «Hello, World!», а затем переходит на новую строку.
Имена переменных и объявления
Имена переменных Go подчиняются простому правилу: имя начинается с буквы или знака подчеркивания и может иметь любое количество дополнительных букв, цифр и знаков подчеркивания.
В Go есть 25 ключевых слов, таких как if и for that, которые можно использовать только там, где это позволяет синтаксис; их нельзя использовать в качестве имен. Эти зарезервированные ключевые слова показаны ниже:
break case chan const continue default defer else fallthrough for func go goto if import interface map package range return select struct switch type var
Объявление var создает переменную определенного типа, присваивает ей имя и устанавливает ее начальное значение. Каждое объявление имеет общий вид
var name type = expression
Часть type или = expression может быть опущена, но не обе. Если тип опущен, он определяется выражением инициализатора. Если выражение опущено, начальным значением является нулевое значение для типа, которое равно 0 для чисел, false для логических значений, "" для строк и nil для интерфейсов и ссылочных типов (слайс, указатель , карта, канал, функция). Не пугайтесь новых концепций. Вы познакомитесь с ними один за другим в этой статье. Короче говоря, механизм нулевого значения гарантирует, что переменная всегда содержит четко определенное значение своего типа; в Go нет такой вещи, как неинициализированная переменная. Например:
var b bool fmt.Println(b) // false printed
В Go имя переменной помещается перед базовым типом. Это отличается от большинства языков. Можно объявить и при необходимости инициализировать набор переменных в одном объявлении с соответствующим списком выражений. Отсутствие типа позволяет объявить несколько переменных разных типов:
var s1 s2 s3 string // string string string var num, str, bo = 4.5, "San Francisco", false // float64, string, // boolean
Набор переменных также можно инициализировать, вызвав функцию, возвращающую несколько значений (функции с несколькими возвращаемыми значениями будут рассмотрены позже):
var resp, err = http.Get(url) // http.Get(url) returns a response // and an error. error is one type of Go variables, same as string, // and int
Короткие объявления переменных
Внутри функции для объявления и инициализации локальных переменных может использоваться альтернативная форма, называемая коротким объявлением переменной. Он принимает форму имя := выражение, а тип имя определяется типом выражения. Например:
name := "John Doe" // string num := 0 // int weight := 135.6 // float64 truth := true // bool
Из-за своей краткости и гибкости короткие объявления переменных используются для объявления и инициализации большинства локальных переменных.
указатели
переменная – это часть памяти, содержащая значение. Значение указателя — это адрес переменной. Если переменная объявлена как var s string, выражение &s («адрес s») возвращает указатель на строковую переменную, то есть значение типа *string, которое произносится как «указатель на строку». Если значение называется ps, мы говорим: «ps указывает на s» или, что то же самое, «ps содержит адрес s». Переменная, на которую указывает ps, записывается как *ps. Выражение *ps возвращает значение этой переменной, int, но поскольку *ps обозначает переменную, оно может также появиться в левой части присваивания, и в этом случае присваивание обновляет переменную.
s := "Blockchain" ps := &s // ps points to s fmt.Println(*ps) // "Blockchain" *ps = "Ethereum" // equivalent to s = "Ethereum" fmt.Println(s) // "Ethereum"
Объявления типов
Тип переменной или выражения определяет характеристики значений, которые может принимать int, например их размер, внутреннее представление, внутренние операции, которые можно выполнять над ними, и методы, связанные с ними.
Объявление типа — действительно крутая функция в Go. Объявление типа определяет новыйименованный тип, который имеет тот же базовый тип, что и существующий тип. Именованный тип позволяет разделить различные и, возможно, несовместимые варианты использования базового типа, чтобы их нельзя было непреднамеренно перепутать.
type name underlying-type
Чтобы проиллюстрировать объявления типов, давайте превратим разные шкалы длины в разные типы:
// Package lengthConv performs miles and kilometers conversion package lengthConv import "fmt" type Kilometers float64 type Miles float64 func KToM(k Kilometer) Miles { return Miles(k / 1.609344) } func MToK(m Miles) Kilometers { return Miles(m * 1.609344) }
Этот пакет определяет два типа, километры и мили, для двух единиц длины. Несмотря на то, что оба имеют один и тот же базовый тип, float64, они не являются одним и тем же типом, поэтому их нельзя сравнивать или комбинировать в арифметических выражениях. Различение типов позволяет совершать ошибки, такие как непреднамеренное объединение температур в двух разных шкалах; для преобразования из float64 требуется явное преобразование типа, такое как Kilometers(t) или Miles(t). Что касается функций KToM и MToK, возвращаемый тип функций помещается перед фигурными скобками, что отличается от большинства языков.
Преимущества использования именованного типа многочисленны. См. код ниже:
package main import "fmt" type students []string type teachers []string func main() { s := students{"Michael Chen", "Lily Green"} t := teachers{"Emily Cohen", "Nat Fisch"} s.printNames() t.printNames() } func (s students) printNames() { for i := 0; i < len(s); i++ { fmt.Println("I am student: ", s[i]) } } func (t teachers) printNames() { for i := 0; i < len(t); i++ { fmt.Println("I am teacher: ", t[i]) } }
Мы объявили два типа, студенты и преподаватели одного и того же базового типа []string. Мы также объявили две функции printNames. Первая функция printNames имеет параметр student s, который появляется перед именем функции. (s студентов) printNames() связывает с типом студентов метод с именем printNames, который печатает имена всех студентов. В будущем вы можете вызывать функцию printNames напрямую, используя s.printNames(), если s — это тип студентов. Тип студентов также называется приемником функции printNames. Последняя функция имеет то же имя printName, что и предыдущая функция. Этот тип перегрузки функций разрешен в Go, потому что у функций разные получатели функций (ученики и учителя).
Объявление типа — важная концепция в Go. Мы будем сталкиваться с этим снова и снова. Мы вернемся к теме объявления типа, когда будем обсуждать реализацию интерфейса в Go.
Структуры данных
Основные типы данных
Основные типы данных в Go включают целые числа, числа с плавающей запятой, строки, логические значения и т. д. Одной из интересных особенностей Go является встроенная поддержка комплексных чисел. Go предоставляет два размера комплексных чисел, комплексные64 и комплексные128, компоненты которых — float32 и float64 соответственно. Комплекс встроенных функций создает комплексное число из его вещественных и мнимых компонентов, а встроенные функции вещественного и имага извлекают эти компоненты:
x := complex(5, 7) // 5+7i y := complex(6, 8) // 6+8i fmt.Println(x * y) // "(-26+82i)" fmt.Println(real(x * y)) // "-26" fmt.Println(imag(x * y)) // "82"
Объявления x и y выше можно упростить:
x := 5 + 7i // 5+7i y := 6 + 8i // 6+8i
Составные типы
Мы рассмотрим четыре составных типа Go: массивы, срезы, карты и структуры.
Массивы
Массив представляет собой последовательность фиксированной длины из нуля или более элементов определенного типа. Из-за их фиксированной длины массивы редко используются напрямую в Go. Срезы, которые могут увеличиваться и уменьшаться, гораздо более универсальны, но чтобы понять срезы, мы должны сначала понять массивы. Встроенная функция len возвращает количество элементов в массиве.
var s [10]string // array of 10 strings s[0] = "John" // assign "John" to the first element fmt.Println(s[0]) // "John" fmt.Println(s[len(s)-1]) // "" last element
Фрагменты
Срезы представляют собой последовательности переменной длины, все элементы которых имеют один и тот же тип. Тип среза записывается []T, где элементы имеют тип T; это похоже на тип массива без размера.
Массивы и срезы тесно связаны. Срез — это легкая структура данных, которая дает доступ к подпоследовательности (или, возможно, ко всем) элементам массива, известного как базовый массив среза. Срез состоит из трех компонентов: указателя, длины и емкости. Указатель указывает на первый элемент массива, доступный через срез, который не обязательно является первым элементом массива. Длина — это количество элементов среза; он не может превышать емкость, которая обычно представляет собой количество элементов между началом среза и концом базового массива. Встроенные функции len и cap возвращают эти значения.
Литерал среза объявляется так же, как литерал массива, за исключением того, что вы опускаете количество элементов:
letters := []string{"a", "b", "c", "d","e", "f"}
Нулевое значение среза равно нулю. Обе функции len и cap возвращают 0 для нулевого фрагмента.
Слайс также может быть сформирован путем «нарезки» существующего слайса или массива. Нарезка выполняется путем указания полуоткрытого диапазона с двумя индексами, разделенными двоеточием. Например, выражение letter[1:4] создает срез, включающий элементы букв с 1 по 3.
Карты
Хеш-таблица — одна из самых оригинальных и универсальных структур данных. Это упорядоченный набор пар ключ/значение, в котором все ключи различны, а значение, связанное с данным ключом, может быть получено, обновлено или удалено с использованием в среднем постоянного числа сравнений ключей, независимо от того, насколько велико значение. большой стол.
В Go карта — это ссылка на хеш-таблицу, а тип карты записывается как map[K]V, где K и V — типы ее ключей и значений. Все ключи в данной карте имеют один и тот же тип, и все значения имеют один и тот же тип, но ключи не обязательно должны быть того же типа, что и значения.
Для создания карты можно использовать встроенную функцию make:
weights := make(map[string]float64) //mapping from strings to float64
Мы также можем использовать литерал карты для создания карты, заполненной некоторыми исходными парами ключ/значение:
weights := map[string]float64{ "Sara": 120.6, "Emma": 115.4, }
Доступ к элементам карты осуществляется с помощью обычной нотации нижнего индекса:
weights["Emma"] = 110.5 fmt.Println(weights["Emma"])
и удаляется встроенной функцией удаления:
delete(weights, "Emma") // remove element weights[John]
Структуры
Структура — это агрегатный тип данных, который объединяет ноль или более именованных значений произвольных типов в виде единой сущности. Каждое значение называется полем. Классический пример структуры из обработки данных — контактная информация клиента, поля которой — номер счета, имя клиента, род занятий, доход, номер телефона, адрес электронной почты и тому подобное. Все эти поля собираются в единую сущность, которую можно копировать как единое целое, передавать функциям и возвращать ими, хранить в массивах и так далее.
Эти два оператора объявляют тип структуры с именем Customer и переменную с именем amelie, которая является экземпляром Customer:
type Customer struct { account int name string occupation string income int phone string email string } var amelie Customer
Доступ к отдельным полям amelie осуществляется с помощью записи через точку, например amelie.name и amelie.occupation. Поскольку amelie является переменной, ее поля также являются переменными, поэтому мы можем присвоить полю:
amelie.phone = "(415)273-6253" // updated the phone number
или взять его адрес и получить к нему доступ через указатель:
income := &amelie.income *income += 20000 // income increased
Порядок полей важен для идентификации типа. Ниже показан один из способов инициализации структуры Customer:
Kate := Customer{ 1029384732, "Kate Witherspoon", "Software Engineer", 200000, "(415)746-3982", "[email protected]", }
Порядок начальных значений должен соответствовать порядку полей. В противном случае инициализация либо не скомпилируется, либо приведет к неожиданным результатам. Чтобы избежать непреднамеренной ошибки такого типа, мы предлагаем другой способ инициализации структуры Customer:
kate := Customer{ account: 1029384732, name: "Kate Witherspoon", occupation: "Software Engineer", income: 200000, email: "[email protected]", phone: "(415)746-3982", }
выплевывая имена полей, мы избегаем присвоения значения неправильному полю. Делая это, нам также не нужно беспокоиться о порядке полей. Вы заметите, что я изменил порядок инициализации полей электронной почты и телефона. Это совершенно нормально. Для инициализации рекомендуется явно записывать имена полей.
Структура также является своего рода типом. Таким образом, он также может служить приемником функций. См. следующий пример:
package main import ( "fmt" ) // Customer type contains essential information about a customer type Customer struct { account int name string occupation string income int phone string email string } func main() { kate := Customer{ account: 1029384732, name: "Kate Witherspoon", occupation: "Software Engineer", income: 200000, email: "[email protected]", phone: "(415)746-3982", } kate.print() } // Customer type is associatd with the print function func (c Customer) print() { fmt.Printf("%+v\n", c) }
Функции
Объявления функций
Объявление функции имеет имя, список параметров, необязательный список результатов и тело:
func name(parameter-list) (result-list) { body }
Если функция возвращает один безымянный результат или вообще не возвращает никаких результатов, круглые скобки необязательны и обычно опускаются. Полное отсутствие списка результатов объявляет функцию, которая не возвращает никакого значения и вызывается только для своих эффектов. В функции rightTriangleArea
func rightTriangleArea(x, y float64) float64 { return 0.5 * x * y } fmt.Println(rightTriangleArea(3, 4)) // 6
x и y — параметры в объявлении, 3 и 4 — аргументы вызова, а функция возвращает значение float64.
Несколько возвращаемых значений
В Go функция может возвращать более одного результата. Существует много примеров функций из стандартных пакетов, которые возвращают два значения: желаемый результат вычислений и значение ошибки или логическое значение, указывающее, сработало ли вычисление. Следующая функция checkIn возвращает имя сотрудника и время, когда он/она зарегистрировался на работе.
func main() { name, checkInTime := checkIn("Liz") fmt.Println(name, "checked in at ", checkInTime) } func checkIn(name string) (string, time.Time) { return name, time.Now() }
Функция checkIn возвращает два значения, включая имя сотрудника и время регистрации сотрудника. Следующая функция checkURL использует стандартную функцию выборки HTTP-запроса, которая возвращает ответ и ошибку и соответствующим образом обрабатывает ответ:
func main() { checkURL("https://google.com") } func checkURL(url string) { resp, err := http.Get(url) if err != nil { fmt.Fprintf(os.Stderr, "fetch: %v\n", err) os.Exit(1) } if _, err := io.Copy(os.Stdout, resp.Body); err != nil { fmt.Fprintf(os.Stderr, "fetch: reading %s: %v\n", url, err) os.Exit(1) } resp.Body.Close() }
Функция http.Get() возвращает ответ и ошибку. Если в процессе выборки что-то пойдет не так, err не равно nil, и мы должны распечатать ошибку как есть и выйти из приложения. Если процесс извлечения завершится успешно, мы хотели бы напечатать тело ответа на стандартный вывод, что выполняется с помощью io.Copy(os.Stdout, соответственноBody). Если в процессе печати что-то пойдет не так, мы хотели бы напечатать ошибку как есть и выйти из приложения. После того, как тело ответа будет успешно напечатано на стандартный вывод, мы хотели бы немедленно закрыть тело ответа, чтобы предотвратить утечку ресурсов.
Передать по значению или передать по ссылке
В Go, когда переменные передаются в вызовы функций, нам нужно быть осторожными, потому что некоторые типы переменных передаются по значению, а некоторые переменные передаются по ссылке. Если переменные передаются по значению, то любые изменения, внесенные в эту переменную внутри вызывающей функции, не изменят исходные значения. Однако вы можете изменить значения, используя указатели в вызывающей функции. На следующей диаграмме показано, какие типы переменных передаются по значению, а какие — по ссылке.
Поток управления
Наиболее важные операторы потока управления включают операторы if, for, while и switch.
Давайте сначала рассмотрим оператор if. Глядя на приведенный выше фрагмент кода, оператор if очень похож на оператор Java или JavaScript, за исключением отсутствия круглых скобок вокруг условия if. См. код ниже:
func ifFlow(height int) { if height > 185 { fmt.Println("This person is really tall") } else { fmt.Println("This person has normal height ") } }
Go также позволяет простому оператору, такому как объявление локальной переменной, предшествовать условию if. Следующий код абсолютно легитимен в Go:
if err := doSomething(); err != nil { log.Print(err) }
Мы могли бы написать это как
err := doSomething() if err != nil { log.Print(err) }
но объединение операторов короче и уменьшает область действия переменной err, что является хорошей практикой.
Перейдем к оператору for. Есть несколько способов реализовать цикл for в Go. Посмотрите на фрагмент кода ниже:
func forFlow(students []string) { // traditional for loop for i := 0; i < len(students); i++ { fmt.Println(students[i]) } // range way for _, value := range students { fmt.Println(value) } }
Традиционный способ реализации цикла for прост. Давайте сосредоточимся на использовании ключевого слова range для реализации ключевого слова. Range — это зарезервированное ключевое слово в Go. Range перебирает элементы в различных структурах данных и соответственно дает разные результаты. Например, диапазон на карте перебирает пары ключ/значение, а диапазон на массивах и срезах предоставляет как индекс, так и значение для каждой записи. В приведенном выше фрагменте кода диапазон для учащихся предоставляет как индекс, так и значение для каждого учащегося. Обратите внимание, что индекс каждого студента не используется в нашем коде. Поскольку в Go не может быть неиспользуемой объявленной переменной, Go предоставляет пустой идентификатор _ (подчеркивание) для замены этой переменной.
Цикл while в Go сложен из-за того, что в Go нет ключевого слова while. В Go мы используем оператор for для реализации цикла while. См. фрагмент кода ниже:
func whileFlow() { i := 0 for i < 10 { fmt.Println(i) i++ } }
Условие цикла while ставится после ключевого слова for. Другая часть кода довольно проста.
Последним, но не менее важным потоком управления является оператор switch. См. фрагмент кода ниже:
func switchFlow() { heads := 0 tails := 0 switch coinFlip() { case "heads": heads++ case "tails": tails++ default: fmt.Println("landed on edge") } fmt.Println(heads) } func coinFlip() string { // create a random number generator r := rand.New(rand.NewSource(99)) if r.Float32() < 0.5 { return "heads" } return "tails" }
Подобно Java или JavaScript, после ключевого слова switch следует выражение, за исключением того, что вокруг выражения нет круглых скобок. В приведенном выше коде выражение является вызовом функции. Выражение не обязательно требуется в операторе switch. Если выражение отсутствует, оператор switch превращается в оператор if/else. См. код ниже:
func switchFlowNoExpression(x int) { switch { case x > 0: fmt.Println(x, " is larger than 0") case x < 0: fmt.Println(x, " is smaller than 0") default: fmt.Println(x, " is equal to 0") } }
Интерфейсы
Многие объектно-ориентированные языки имеют некоторое представление об интерфейсах, но что делает интерфейсы Go такими отличительными, так это то, что они выполняются неявно[1]. Нет необходимости объявлять все интерфейсы, которым удовлетворяет данный конкретный тип; достаточно просто владеть необходимыми методами.
Интерфейсы как контракты
Все типы, которые мы рассматривали до сих пор, были конкретными типами. Конкретный тип определяет точное представление своих значений и предоставляет внутренние операции этого представления.
В Go есть еще один тип типа, называемый типом интерфейса. Интерфейс является абстрактным типом. Он не определяет представление внутренней структуры своих значений или набор основных операций, которые они поддерживают; он раскрывает только некоторые из их методов.
Типы интерфейсов и удовлетворение
Тип интерфейса определяет набор методов, которыми должен обладать конкретный тип, чтобы считаться экземпляром этого интерфейса. Интерфейс содержит только сигнатуры методов, но не их реализации. Следующий интерфейс Animal определяет функцию getGreeting:
type Animal interface { getGreeting() string }
Функция getGreeting не принимает никаких параметров, но возвращает значение строкового типа.
Чтобы конкретный тип удовлетворял интерфейсу, он должен обладать всеми методами, требуемыми интерфейсом. Например, *os.File удовлетворяет требованиям io.Reader, Writer, Closer и ReadWriter. Для краткости программисты Go часто говорят, что конкретный тип «является» конкретным типом интерфейса, что означает, что он удовлетворяет интерфейсу. См. следующий простой пример:
package main import ( "fmt" ) // Animal interface type Animal interface { getGreeting() string } // Dog type satisfies Animal interface type Dog struct { } // Cat type satisfies Animal interface type Cat struct { } func main() { d := Dog{} c := Cat{} printGreeting(d) printGreeting(c) } func printGreeting(a Animal) { fmt.Println(a.getGreeting()) } func (Dog) getGreeting() string { // return various dog barking sounds return "woof-woof, arf-arf, ruff-ruff, bow-wow" } func (Cat) getGreeting() string { // return cat sound return "Meow" }
Интерфейс Animal включает только функцию getGreeting. Поскольку обе структуры Dog и Cat связаны с функцией getGreeting, они неявно удовлетворяют интерфейсу Animal. Таким образом, структуры Dog и Cat могут быть переданы в функцию printGreeting, и соответственно будет вызвана определенная версия функции getGreeting. Интерфейс делает код более понятным и предоставляет удобный API между пакетами или клиентами (пользователями) и серверами (поставщиками). Интерфейс здесь подробно не рассматривается, поэтому не стесняйтесь обращаться к [1] для более подробного обсуждения интерфейса.
Горутины и каналы
Параллельное программирование никогда не было более важным. Например, веб-серверы обрабатывают тысячи запросов одновременно. Одновременное выполнение более чем одной задачи называется параллелизмом[2]. Изящно решите проблему параллелизма с помощью горутин и каналов.
Горутины
Горутина — это функция, которая может работать одновременно с другими функциями. Чтобы создать горутину, мы используем ключевое слово go, за которым следует вызов функции:
package main import ( "fmt" "math/rand" ) func flipCoin(n int) { for i := 0; i < n; i++ { if rand.Float64() < 0.5 { fmt.Println("Heads") } else { fmt.Println("Tails") } } } func main() { go flipCoin(20) var input string fmt.Scanln(&input) }
Эта программа состоит из двух горутин. Первая горутина неявна и сама является основной функцией. Вторая горутина создается, когда мы вызываем функцию go flipCoin(). Обычно, когда мы вызываем функцию, наша программа выполняет все инструкции в функции, а затем возвращается к следующей строке после вызова. С горутиной мы сразу же возвращаемся к следующей строке и не ждем завершения функции. Вот почему был включен вызов функции Scanln; без него программа завершилась бы до того, как получила бы возможность распечатать все результаты перелистывания.
Вы можете легко создать несколько горутин и выполнять их одновременно. В следующем примере вы увидите, что несколько горутин действительно могут работать одновременно:
package main import ( "fmt""time""math/rand" ) func printNumbers(n int) { for i := 0; i < 5; i++ { fmt.Println(n, ":", i) amt := time.Duration(rand.Intn(50)) time.Sleep(time.Millisecond * amt) } } func main() { for i := 0; i < 5; i++ { go printNumbers(i) } var input string fmt.Scanln(&input) }
printNumbers выводит числа от 0 до 5 с ожиданием от 0 до 50 мс после каждого. stdout показывает, что горутины действительно работают одновременно.
каналы
В предыдущих двух примерах мы добавили дополнительную строку fmt.Scanln(&input) в основную функцию, чтобы дождаться завершения горутин перед выходом из основной горутины. Мы также упомянули, что нам нужна дополнительная строка кода, потому что основная горутина не имеет информации о других горутинах. Когда основная горутина завершается, все горутины резко завершаются, и программа завершается. Весьма вероятно, что некоторые горутины еще не завершены. В Go каналы позволяют двум горутинам общаться друг с другом и синхронизировать их выполнение. Вот пример использования каналов:
package main import ( "fmt" "io" "io/ioutil" "net/http" "time" ) func main() { urls := []string{ "https://google.com", "https://amazon.com", "https://facebook.com", "https://stackoverflow.com", } start := time.Now() ch := make(chan string) for _, url := range urls { go fetch(url, ch) } for range urls { fmt.Println(<-ch) } fmt.Printf("%.2fs passed\n", time.Since(start).Seconds()) } func fetch(url string, ch chan string) { start := time.Now() resp, err := http.Get(url) if err != nil { ch <- fmt.Sprint(err) // send to channel ch return } nbytes, err := io.Copy(ioutil.Discard, resp.Body) resp.Body.Close() // don't leak resources if err != nil { ch <- fmt.Sprintf("while reading %s: %v", url, err) return } secs := time.Since(start).Seconds() ch <- fmt.Sprintf("%.2fs %7d %s", secs, nbytes, url) }
Эта программа будет делать запросы на выборку к веб-сайтам и записывать, сколько времени занимает процесс выборки, а также размер тела ответа. Тип канала представлен ключевым словом chan, за которым следует тип вещей, которые передаются по каналу (в данном случае мы передаем строки). Оператор ‹- (стрелка влево) используется для отправки и получения сообщений на канале. «ch ‹- fmt.Sprint(err)» означает отправку «fmt.Sprint(err)» на канал ch. «fmt.Println(‹-ch)» означает распечатку всего, что приходит из канала ch. В основной функции есть цикл for, который выводит сообщения, поступающие из канала, в порядке поступления. Тестовый запуск программы дает следующий результат:
0.14s 10732 http://google.com 0.51s 259635 http://stackoverflow.com 0.66s 480337 http://facebook.com 1.04s 533866 http://amazon.com 1.04s passed
Поскольку горутины выполняются одновременно, общее время выполнения основной горутины самое большое среди всех горутин.
Резюме
Изучать Go и применять его на практике было очень весело. Эта статья охватывает только верхушку айсберга Го. Я надеюсь, что вы узнали что-то новое из моей статьи, и вам не терпится приступить к изучению Go. Продолжай учиться.
Свяжитесь со мной в Твиттере, чтобы получать больше статей и обновлений:
@пандуфао
https://twitter.com/pangdufao?lang=en
Использованная литература:
[1] Алан А. А. Донован и Брайан В. Керниган, Язык программирования Go, 2016 г.
[2] Калеб Докси, Введение в программирование на Go, 2012 г.