Великие люди в Google разработали замечательный язык под названием Go . На первый взгляд кажется, что в лучшем случае Руби и Го — дальние родственники. Тем не менее, их дополнительные наборы навыков обеспечивают идеальное соответствие.
Определенно стоит того, чтобы любой Rubyist взглянул на Go, так как некоторые из нововведений, которые он предлагает, довольно заманчивы.
Для меня Go был недостающим звеном между C ++ и Ruby. Особенно при написании серверов, которые должны быть быстрыми на ногах, я часто выбирал C ++, и мне не хватало тонкостей Ruby. Я хотел бы написать Ruby вместо этого, но даже с недавними, значительными улучшениями производительности, Ruby не может справиться.
Иди заполняет этот пробел. Он обеспечивает ощущение динамического языка, такого как Ruby или Python, но имеет производительность, сравнимую со многими его скомпилированными родственниками.
Он также имеет некоторые действительно уникальные функции, которые мы рассмотрим в этой статье. Давай прыгнем.
Зачем идти?
При написании сервера открытие одного потока для каждого клиента в качестве средства параллелизма (если все это звучит как бред, это нормально, продолжайте чтение) ужасно, когда нужно обрабатывать множество клиентов. Опция заключается в том, чтобы использовать то, что называется неблокирующим вводом-выводом (палец вверх до толпы узлов) Но даже в Unix-системах (например, Linux, Mac OS X и т. Д.) Механизм эффективной обработки неблокирующих операций ввода-вывода отличается. Затем, в дополнение ко всем этим осложнениям, есть и сам C. Я не имею ничего против C для встроенных устройств или где скорость работы значительно опережает время разработки. Но, как повседневный язык, C не подходит для меня.
Go предоставляет удивительные примитивы параллелизма, хороший синтаксис, хорошо укомплектованную библиотеку по умолчанию и быстрый компилятор. Это решает проблемы, которые у меня были с C (а также C ++, в некоторой степени). Это интересно использовать, даже когда ваша кодовая база становится больше.
В этой статье я расскажу об основах языка в вихревом туре, часто указывая на документацию. Основное внимание уделяется инновационным особенностям языка, которые делают его таким уникальным.
Тедий
Go означает не удивлять; это должно прийти легко. Вот основная схема кода:
package main func main() { }
Основная функция вызывается, чтобы начать нас. Давайте попробуем «Привет, мир:»
package main import "fmt" func main() { fmt.Println("Hello, world!") }
Модуль для печати и сканирования называется «fmt» в Go. В отличие от Ruby, он не включен по умолчанию, поэтому мы добавили оператор import в начало файла, чтобы включить его. Метод Println
из модуля fmt
выведет строку, которую мы передаем, и символ новой строки (похожий на puts
в Ruby). Обратите внимание, что публичные методы в Go начинаются с заглавной буквы.
Некоторые быстрые циклы:
package main import "fmt" func main() { //the basic for loop for i:=1; i < 100; i++ { fmt.Println(i) } }
У Go и Ruby совершенно другое представление о цикле for
. Версия Go более или менее похожа на C. Вы определяете переменную, проверяете условие и указываете, что делать в конце одной итерации цикла (в данном случае, увеличивая i
). Это в основном единственный вид цикла, который определяет Голанг. К счастью, это чрезвычайно универсально. Например, это вечный цикл:
for { }
Я бы посоветовал вам ознакомиться с некоторыми (документация for
) [http://golang.org/doc/effective_go.html#for].
Обратите внимание, что когда мы устанавливаем значение i
в цикле for
, мы не используем «=», вместо этого мы используем «: =». Вот пример разницы:
package main import "fmt" func main() { //defines the variable a a := 5 fmt.Println(a) //sets a different value to a a = 10 fmt.Println(a) //another way to define a variable var b int b = 15 fmt.Println(b) }
В первом фрагменте main
функции переменная a
объявляется и присваивается. Во втором случае значение переназначается, поэтому используется =
. Причина заключается в том, что Go на самом деле является языком статической типизации, в отличие от Ruby, который динамически типизирован. Таким образом, компилятор должен знать, где объявлены переменные, а где их содержимое просто заменено. Это становится ясно с помощью третьей части функции main
, которая явно объявляет и устанавливает значение переменной с помощью ключевого слова var
.
Наконец, как и в случае с массивами в Ruby, в Go есть фрагменты. Они имеют тип type []type
, где type
представляет тип объекта, который мы хотим, чтобы срез возвращал. Их использование немного странно:
package main func main { ///this creates a slice of integers with length 15 mySlice := make([]int, 15) }
Мы должны использовать вызов make()
, чтобы сделать срез.
Вероятно, это был очень быстрый тур по некоторым из основных функций Go; Я бы лучше потратил больше времени на некоторые интересные функции, а не на основной синтаксис, который хорошо документирован .
Давайте проверим goroutines.
Goroutines
Написание параллельного кода сложно. Написание параллельного сетевого кода еще сложнее. Проблема в том, что традиционные потоки плохо масштабируются, и с ними чрезвычайно трудно работать, если у вас есть несколько запущенных потоков. Команда Go решила решить эту проблему, производя goroutines.
По сути, goroutines — это облегченные механизмы параллелизма, которые взаимодействуют друг с другом с помощью конструкций, называемых каналами. Они невероятно просты в использовании:
package main import "fmt" func wait() { //wait around with a forever loop for { } } func main() { go wait() fmt.Println("We didn't wait because it was called as a goroutine!") }
У нас есть метод wait
который должен быть бесконечным циклом. Но мы вызываем метод как go wait()
, а не просто wait()
. Это говорит Go, что мы хотим, чтобы он назывался goroutine и выполнялся асинхронно! Запуск программы не создает вечный цикл, поскольку цикл выполняется в фоновом режиме.
Итак, Go имеет параллелизм, встроенный в язык, то есть он имеет примитивы параллелизма. Но какой в этом смысл? Отсутствие необходимости включать библиотеку или модуль не кажется большой проблемой. Но горутины в корне отличаются от потоков, потому что они намного легче. Помните, как не следует писать серверы, которые используют один поток для каждого клиента? С горутинами ситуация иная:
package main import ( "fmt" "net" ) //notice that in the arguments, the name of //the variable comes first, then comes the //type of the variable, just like in "var" //declarations func manageClient(conn net.Conn) { conn.Write([]byte("Hi!")) conn.Close() //do something with the client } func main() { //we are creating a server her that listens //on port 1337. Notice that, similar to Ruby, //a method can have two return values (although //in Ruby, this would be an array instead) listener, err := net.Listen("tcp", ":1337") for { //accept a connection connection, _ := listener.Accept() go manageClient(connection) } }
Вау. Это может показаться немного сложнее, но идея очень проста. Давайте разберем это шаг за шагом.
Глядя на main
функцию, начните с вызова метода net.Listen
, который возвращает два значения (в Go вы не ограничены одним значением, аналогично распаковке массива Ruby) — соединение с сервером и ошибка. Затем войдите в основной цикл сервера, где он просто сидит и слушает запросы с server.Accept
. server.Accept
вызов. Этот звонок «зависает», пока клиент не подключится. Затем, как только мы подключимся, передайте объект подключения в manageClient
, но вызовите его как программу! Итак, сервер готов к обработке следующего клиента.
Наконец, обратите внимание на пару вещей о методе manageClient
. Во-первых, в списке аргументов имя переменной стоит первым, а тип — после. Это более или менее стилистический выбор, который сделали создатели Go; Вы даже не заметите это через неделю или около того с языком.
В теле метода напишите клиенту сообщение «Привет!» И закройте сокет.
Итак, с помощью нескольких строк кода мы создали разумную основу для простого сервера. Без особых усилий вы можете превратить это в HTTP-прокси (бонусные баллы, если вы реализуете некоторое кэширование). Goroutines позволяют нам сделать это. Они на самом деле не просто легкие нити; за кулисами происходит много магии, которая позволяет писать простой императивный код в процедурах.
каналы
Горутины сами по себе полезны, но их полезность значительно усиливается за счет концепции каналов. Они служат механизмом связи между программами и основным процессом. Давайте посмотрим на очень быстрый пример.
package main import ( "fmt" ) var eventChannel chan int = make(chan int) func sayHello() { fmt.Println("Hello, world!") //pass a message through the eventChannel //it doesn't matter *what* we actually send across eventChannel < - 1 } func main() { //run a goroutine that says hello go sayHello() //read the eventChannel //this call blocks so it waits until sayHello() //is done <- eventChannel }
У нас есть программа под названием sayHello
которая распечатывает сообщение «Hello, world». Но обратите внимание на объявление eventChannel
. По сути, мы объявили канал целых чисел. Мы можем посылать материал на этот канал, а другие части кода могут читать с этого канала, что делает его методом связи. В методе sayHello
eventChannel pushes the integer
1
в eventChannel
. Затем в main
методе мы читаем из eventChannel.
Здесь есть важный момент: по умолчанию, чтение из блоков каналов, поэтому он продолжает ждать, пока что-то может быть прочитано из eventChannel.
Давайте посмотрим на что-то более сложное:
package main import ( "fmt" ) var logChannel chan string = make(chan string) func loggingLoop() { for { //wait for a message to arrive msg := < - logChannel //log the msg fmt.Println(msg) } } func main() { go loggingLoop() //do some stuff here logChannel <- "messaged to be logged" //do other stuff here }
Здесь мы определили main
цикл, который висит вокруг прослушивания событий, то есть loggingLoop
. Когда он получает сообщение для регистрации из loggingChannel
, он распечатывает его. Это довольно распространенный дизайн, особенно когда в цикле событий есть какое-то состояние.
Итак, с помощью нескольких строк кода мы установили связь между основной функцией и программами. Совместно используемая память традиционно была кошмаром для разработчиков из-за таких проблем, как блокировка, условия гонки и т. Д. В Go концепция каналов значительно снижает вероятность большинства традиционных проблем. Кроме того, каналы Go являются неотъемлемой частью языка; не просто что-то связанное с библиотекой.
По сравнению с Ruby, программы Go на самом деле работают в фоновом режиме с реализацией языка «по умолчанию» (MRI Ruby работает полностью в пределах одного потока, поэтому он не может обеспечить «настоящий» параллелизм). Во-вторых, Ruby поставляется с поточной реализацией, но ее использование — непростая задача. На самом деле, библиотека агента пытается получить некоторые тонкости горутин в мире Ruby.
Завершение (пока)
Мы рассмотрели много вопросов в этой статье. Мы пробежались по самому базовому синтаксису кода Go, а затем быстро погрузились в механизмы параллелизма.
Оставайтесь с нами для части 2, где мы углубимся в синтаксис и обсудим некоторые другие замечательные функции, которые дает нам Go.