LXF131:GoogleGo

Материал из Linuxformat.

Перейти к: навигация, поиск

Содержание

Go: новый уровень

В прошлый раз мы разобрались в основах синтаксиса Go. Сегодня Андрей Боровский займется более сложными аспектами языка.


Go

Является ли богатство синтаксических конструкций признаком хорошего языка программирования? Я думаю, нет. Очень часто это многообразие возникает в результате того, что изначальный синтаксис был не очень удачным. Стремясь исправить положение, разработчики добавляют новые формы, но старые, уже используемые, удалить не могут. Впрочем, избыточность свидетельствует и о долгой истории развития языка. Go молод, и его разработчики имеют богатый практический опыт, а значит, утверждая, что в Go отсутствуют «пережитки» C и C++, они, скорее всего, правы. Возможно, именно поэтому скромный набор базовых средств языка Go не создает при работе ощущения скованности. С другой стороны, лаконичный синтаксис Go лишает нас удовольствия поломать голову над конструкциями типа

i[++i] = ++i [i++]

(операторы инкремента и декремента присутствуют в Go только в постфиксной форме).

Ваш пакет

Строго говоря, мы уже создавали свой пакет месяц назад – любая Go-программа состоит минимум из одного. Синтаксис описания разделяемых пакетов тот же самый; нужно лишь помнить, что по правилам Go экспортируемые из пакета идентификаторы должны начинаться с заглавной буквы. Для примера рассмотрим пакет threads, содержащий набор функций для работы с потоками.

package threads
 var c = make(chan int, 1)
 var counter int = 0
 func NewThread() { 
 	 counter++
 }
 func ExitThread() {
 	 counter--
 	 c <- 1
 }
 func WaitForThreadsLessThan(n int) {
 	 for counter >= n {
 		 <- c
 	 }
 }
 func Join() {
 	 WaitForThreadsLessThan(1)
 }
 func ThreadsCount() int {
 	 return counter
 }

Функция WaitForThreadsLessThan() приостанавливает выполнение вызвавшего ее потока до тех пор, пока число запущенных потоков (переменная counter) не станет меньше заданного значения. Для этого используется механизм синхронизации с помощью буферизованных каналов (LXF130). Чтобы это работало, перед запуском каждого нового потока нужно вызвать NewThread(), а перед завершением – ExitThread().

Протестируем наш пакет на простой многопоточной программе – сетевом сервере (да, в Go многопоточный сетевой сервер действительно может быть простой программой).

package main
 import (
 net “net”
 flag “flag”
 fmt “fmt”
 threads “./threads”		
 )
 const max_threads int = 2
 func reply(connection * net.TCPConn) {
 	 buffer := make([]byte, 255)
 	 connection.Read(buffer)
 	 connection.Write(buffer)
 	 connection.Close()
 	 threads.ExitThread()
 }
 func main() {
 	 flag.Parse()
 	 if flag.NArg() != 1 {
 		 fmt.Printf(“Использование: ./server <address>\n ”);
 	 }
 
 	 addr, e := net.ResolveTCPAddr(flag.Arg(0) + “:7)
 	 if e != nil {
 		 fmt.Printf(“Ошибка привязки адреса (%s)”, e)
 		 return
 	 }
 	 sock, e := net.ListenTCP(“tcp”, addr)
 	 if e != nil {
 		 fmt.Printf(“Ошибка инициализации сервера (%s)”, e)
 		 return
 	 }
 	 for {
 		 threads.WaitForThreadsLessThan(max_threads)
 		 conn, e := sock.AcceptTCP()
 		 if e != nil {
 			 fmt.Printf(“Ошибка соединения (%s)”, e)
 			 return
 		 }
 		 threads.NewThread()
 		 go reply(conn)
 	 }
 }

Многопоточный ECHO-сервер на Go: действительно, работает.

Наш сервер слушает порт 7 (стандартный для протокола ECHO). Получив от клиента запрос на соединение, сервер считывает его первые 255 байт, отправляет их обратно клиенту и закрывает соединение (это не совсем соответствует стандарту протокола ECHO, но нам хватит). Адрес для привязки сервера передается программе в виде аргумента командной строки.

Обмен данными с клиентом выполняет функция reply(), которую мы вызываем как сопроцедуру. Пакет threads нужен для того, чтобы ограничить число одновременно выполняющихся потоков. Использование в программе нашего пакета ничем не отличается от использования стандартного – например, fmt. Единственное, что нужно учесть – это расположение файла пакета. Если он находится не в стандартной директории пакетов Go, в строке импорта пакета мы должны указать полный путь к нему, даже еслифайл пакета находится в той же директории, что и файл программы. Пакет должен быть скомпилирован в объектный код (без вызова компоновщика) перед компиляцией основной программы.

Пакеты могут состоять из нескольких файлов исходных текстов Go (расположенных в одном каталоге), при этом их имена (за исключением одного, естественно) не могут совпадать с именем пакета.

Помимо пакета threads, в нашей программе-сервере используются и другие. Net содержит базовые функции, необходимые для работы с сетью. Flag предоставляет средства для анализа командной строки.

Тонкости многопоточности

Незабвенный доктор Эмет Браун советовал Марти мыслить в четырех измерениях. Работа с потоками также требует особой культуры мышления. На первый взгляд может показаться, что функцию NewThread() следует вызывать первой строчкой в теле функции reply() (такой код смотрелся бы красивее), но в этом случае функция WaitForThreadsLessThan() не смогла бы гарантировать, что число потоков действительно не превышает заданного значения (подумайте, почему).

Многопоточность в Go – настоящая, а не корпоративная, как, например, в Lua, однако количество потоков, которые использует программа Go, как правило, меньше, чем число выполняемых сопроцедур. Разработчики Go попытались найти компромисс между использованием потоков для реализации сопроцедур и накладными расходами на создание нового потока в операционной системе. Предложенное ими решение заключается в том, что несколько сопроцедур разделяют на уровне ОС один поток. Если одна из сопроцедур блокирует поток, остальные связанные с ним сопроцедуры могут быть перенесены в другой. Все это, напомним, происходит в скомпилированной программе по ходу ее работы. Иными словами, программам Go требуется свой менеджер управления потоками времени выполнения. Разработчику обычно не приходятся задумываться о таких вещах, но, поработав с сопроцедурами, вы заметите, что переключение между ними обычно происходит в момент блокировки или при вызове таких функций как fmt.Printf().

Сложные типы

Помимо примитивных типов данных, в Go имеется и ряд сложных. Наряду с уже известными нам каналами, к ним относятся строки, массивы [arrays] и их сечения [slices], структуры, интерфейсы, ассоциативные массивы [maps] и процедурные типы.

Переменные-массивы в Go объявляются так же, как и переменные простых типов:

var a [32] byte

Массивы в Go не являются указателями, как в C, однако память, выделенная массиву элементов простого типа, расположена непрерывно. Таким образом, массив Go можно «конвертировать» в массив C с помощью указателя на первый элемент (это правило важно при работе с функциями, импортируемыми из библиотек C, и оно не распространяется на сечения, которые нельзя преобразовать в массивы C).

Сечения похожи на массивы, за исключением того, что при объявлении переменной этого типа длина массива не указывается. Сечение может быть создано динамически с помощью функции make(). Важное применение сечений – передача массивов в функции. Сечения и массивы совместимы между собой, так что функции, принимающей сечение, можно передать любой массив заданного типа. Эффективность сечений как параметров функций объясняется тем, что это ссылочный тип, который можно рассматривать как указатель на фрагменты массивов с особым синтаксисом. До тех пор, пока значение переменной-сечения не проинициализировано, она равна nil.

Структуры Go очень похожи на таковые в C. Они могут объединять переменные и функции.

struct {
 	 x, y int
 	 u float
 	 A *[]int
 	 F func()
 }

Любопытной особенностью Go является возможность включить в состав структуры «переменные набивки» (в качестве их имени указывается символ _, и обратиться к ним нельзя). Их используют для выравнивания адресов членов структуры по определенным границам, что может понадобиться, например, при взаимодействии с кодом, написанным на C. Скажем, если структура Go содержит двухбайтные переменные, а правила выравнивания в коде C требуют, чтобы адреса всех переменных-членов структуры были кратны 4, поля в структуре Go можно выровнять с помощью переменных набивки.

Те, кому приходилось использовать код C из других языков программирования, знают, сколько проблем могут создать различия в правилах выравнивания адресов переменных в структурах. Однако стоит отметить, что использование переменных набивки не является универсальным решением проблемы. Разные компиляторы на разных платформах могут использовать разные правила выравнивания, так что сопряженный с C код Go может работать на одной платформе и не работать на другой.

Еще одна интересная особенность структур – безымянные переменные. При их объявлении указывается только тип, но не имя. Безымянными членами структур обычно являются другие структуры. Пусть у нас есть некоторая структура t, и мы объявляем структуру S как

type S struct {
 	 t
 	 ...
 }

В этом случае все поля и методы t можно вызывать как поля и методы S. Если вы пользовались GTK+, эта концепция должна быть вам знакома.

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

type Conn interface {
  Read(b []byte) (n int, err os.Error)
  Write(b []byte) (n int, err os.Error)
  Close() os.Error
  LocalAddr() Addr
  RemoteAddr() Addr
  SetTimeout(nsec int64) os.Error
  SetReadTimeout(nsec int64) os.Error
  SetWriteTimeout(nsec int64) os.Error
 }

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

Интерфейсы представляют собой нечто большее, чем просто наборы функций. Пусть I – некоторый интерфейс. Тогда переменная типа I будет совместима с переменными других типов при условии, что множество методов интерфейса I является подмножеством методов этих типов.

Ассоциативные массивы Go позволяют выполнять индексирование переменными различных типов, а не только целочисленными. Например,

a := map [string] int

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

Функции make() и new()

Мы уже встречались с функцией make() в нескольких примерах. Она предназначена для динамического создания каналов, сечений и ассоциативных массивов (следует помнить, что значение, возвращаемое make(), является не указателем на область памяти, а значением переменной). Функция make() не только выделяет область памяти для новой переменной, но и инициализирует ее. В общем виде вызов make() выглядит так:

make(<тип_данных>, <значение инициализации> [,<значение инициализации>] ...)

Параметры инициализации зависят от типа создаваемой переменной.

Функция new() также используется для динамического создания переменных, но работает иначе, чем make(). Единственный аргумент new()  – тип создаваемой переменной. Вызов new(T) возвращает значение типа *T. Выделенная под переменную область памяти заполняется нулями, и никакой дальнейшей инициализации не производится.

Объекты в Go

Go не является объектно-ориентированным языком, однако при необходимости в нем можно создавать нечто вроде объектов. Для примера напишем альтернативную реализацию пакета threads (файл oopthreads.go). Один из недостатков приведенного выше кода заключается в использовании глобальных переменных. Глобальные переменные – не проблема, если нам нужен только один менеджер сопроцедур; но, предположим, требуется управлять несколькими группами сопроцедур так, чтобы для каждой из них была определена своя функция Join() и т. д. Одно из возможных решений – использование структур с методами:

package oopthreads
 type Thread struct {
 	 c chan int
 	 counter int
 }
 func NewThreadObject() * Thread {
 	 t := new(Thread)
 	 t.c = make(chan int, 1)
 	 return t
 }
 func (this * Thread) NewThread() {
 	 this.counter++
 }
 func (this * Thread) ExitThread() {
 	 this.counter--
 	 this.c <- 1
 }
 func (this * Thread) WaitForThreadsLessThan(n int) {
 	 for this.counter >= n {
 		 <- this.c
 	 }
 }
 func (this * Thread) Join() {
 	 this.WaitForThreadsLessThan(1)
 }
 func (this * Thread) ThreadsCount() int {
 	 return this.counter;
 }

Переменные, необходимые для управления потоками, собраны в структуру Thread, но это не главное. Функции, управляющие сопроцедурами, теперь объявлены как методы этой структуры. Объявление функции-метода для некоторого типа, помимо стандартного заголовка, включает в себя параметр-указатель на значение данного типа. Этот параметр объявляется перед именем функции. Например, объявленная выше функция

func (this * Thread) ThreadsCount() int

есть метод структуры Thread, а

func ThreadsCount(this * Thread) int

– нет. Методы структуры являются ее селекторами, то есть их следует вызывать, используя синтаксис

<имя_переменной_структуры>.<имя_метода>

При этом в дополнительном параметре метода передается указатель на переменную, для которой он вызван. Он используется в теле метода для обращения к значению указанной переменной. Описанное очень похоже на реализацию объектов в объектно-ориентированных языках программирования, за исключением того, что в них параметр this создается автоматически.

Особого внимания заслуживает проблема конструкторов. Для динамического создания структур мы должны пользоваться функцией new(), заполняющей выделенный участок памяти нулями. Функцию new() нельзя совместить с вызовом конструктора, как в C++, так что проблему конструкторов придется решать отдельно. Для инициализации структуры можно создать специальный метод, например:

func (this * Thread) init() {
 	 this.c = make(chan int, 1)
 }

В этом случае экземпляр структуры нужно сначала создать с помощью функции new(). В нашем примере используется другой распространенный прием: функция-фабрика объектов NewThreadObject(), возвращающая указатель на уже готовый объект.

Поскольку объект в Go – обычная структура, переменные-объекты можно создавать и статически, и даже инициализировать их в процессе объявления – например, переменная типа Thread может быть объявлена и проинициализирована такой строкой:

t := Thread {make(chan int, 1), 0}

Перепишем наш сервер с использованием пакета oopthreads:

Так выглядит вывод программы 6nm для модуля threads.

package main
 import (
     net “net”
     flag “flag”
     fmt “fmt”
     . “./oopthreads”
  )
 const max_threads int = 1;
 var thread_manager * Thread
 func reply(connection * net.TCPConn) {
       buffer := make([]byte, 255)
       connection.Read(buffer)
       connection.Write(buffer)
       connection.Close()
       thread_manager.ExitThread()
 }
 func main() {
       flag.Parse()
       if flag.NArg() != 1 {
             fmt.Printf(“Использование: ./server <address>\n ”);
    }
    thread_manager = NewThreadObject()
    addr, e := net.ResolveTCPAddr(flag.Arg(0) + “:7)
    if e != nil {
         fmt.Printf(“Ошибка привязки ад реса (%s)”, e)
         return
    }
    sock, e := net.ListenTCP(“tcp”, addr)
    if e != nil {
         fmt.Printf(“Ошибка инициализации сервера (%s)”, e)
         return
    }
    for {
         thread_manager.WaitForThreadsLessThan(max_threads)
         conn, e := sock.AcceptTCP()
         if e != nil {
              fmt.Printf(“Ошибка соединения (%s)”, e)
              return
         }
         thread_manager.NewThread()
         go reply(conn)
    }
 }

Обратите внимание, что в программе, импортирующей модуль oopthreads, мы не можем получить доступ к членам структуры Thread.c и Thread.counter, поскольку имена этих переменных начинаются со строчной буквы. Таким образом достигается некоторая инкапсуляция.

Методы могут быть не только у структур. Взгляните, например, на такой код:

type Sequence []int
 func (s Sequence) Len() int {
   return len(s)
 }
 func (s Sequence) Less(i, j int) bool {
   return s[i] < s[j]
 }
 func (s Sequence) Swap(i, j int) {
   s[i], s[j] = s[j], s[i]
 }

Хотя определение метода требует, чтобы дополнительный параметр был указателем, в приведенном выше примере символ * отсутствует – потому что в определении типа Sequence используется сечение, которое является указателем автоматически.

Необходимо различать методы и функции-члены структур. Методы – это функции, связанные с определенным типом данных, в то время как функции-члены являются обычными переменными типа «указатель на функцию», и им могут быть присвоены любые функции, не связанные ни с каким конкретным типом.

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

type IThread interface {
 	 NewThread()
 	 ExitThread()
 	 WaitForThreadsLessThan(n int)
 	 Join()
 	 ThreadsCount() int
 }
 func NewIThread() * IThread {
 	 i := IThread(NewThreadObject())
 	 return &i
 }

Функция NewIThread() сначала создает объект Thread, а затем приводит его к типу IThread. В результате мы достигаем полной инкапсуляции объекта Thread. Если программы, импортирующие модуль oopthreads, используют исключительно интерфейс IThread, мы можем заменить объект Thread на любой другой со схожим набором методов совершенно незаметно для этих программ. Интерфейсы позволяют нам реализовать для объектов Go некое подобие полиморфизма. Функция, предназначенная для работы с объектами, родственными Thread, может быть объявлена как

func Foo(thread * IThread)

Другие языки

Импортировать функции из библиотек C в программу, написанную на Go, не просто, а очень просто. Правда, для этого потребуются специальные инструменты. Помимо набора утилит для работы Go, которым мы пользовались до сих пор, существует еще один инструментарий, основанный на GCC. Программа gccgo представляет собой головную часть системы GCC, добавляющую Go к набору языков, поддерживаемых GNU Compilers Collection. Для работы с ggcgo вам придется выкачать из репозитория и самостоятельно собрать проект из исходных текстов. Потребуется также специальный компоновщик gold (аналог ld для Go). Вместе с gccgo компилируются и устанавливаются некоторые другие утилиты, также необходимые для сборки. Поскольку их имена совпадают с названиями их стандартных аналогов, я рекомендую устанавливать gccgo в отдельную директорию. После того, как все будет настроено, написать программу, использующую функции C, становится действительно просто. Вот как, например, импортируется функция perror() из стандартной библиотеки C:

package main
 func c_perror(s * byte) __asm__ (“perror”)
 func main() {
   var msg = [5]byte{'t', 'e', 's', 't', 0}
   c_perror(&msg[0])
 }

Имя импортируемой функции указывается в скобках послеключевого слова __asm__ (что похоже на синтаксис встраиваемого ассемблера в GNU C, но отличается по сути). Типу char * из C в Go соответствует массив типа byte. При вызове функции мы передаем ей указатель на первый элемент массива (сечения использовать нельзя). Простые типы вроде int и byte, а также структуры и указатели могут свободно передаваться между C и Go; остальные типы несовместимы. Если заглянуть под капот Go, мы увидим, что сечениям и строкам Go соответствуют простые структуры на C. В принципе, можно написать C-функцию, которая, получив указатель, например, на переменную-сечение Go, будет работать с соответствующей структурой C; однако внутреннее представление типов Go может меняться.

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

Теперь вы знаете достаточно, чтобы начать применять Google Go в своей работе. Конечно, язык пока далек от завершения, и, возможно, выйдет из лабораторий не раньше Perl 6, но все же, если вы реализуете на нем что-то стоящее – дайте нам знать.

Личные инструменты
  • Купить электронную версию
  • Подписаться на бумажную версию