Познакомился с языком программирования Go

21 июня 2012

В сей заметке речь пойдет о языке Go, с которым я имел удовольствие познакомиться несколько недель назад. Будут освещены особенности языка и разобрана небольшая программка на нем. Напоследок я поделюсь своими субъективными впечатлениями от работы с Go.

Изучаем Go «за 24 часа»

Если вам хочется ознакомиться с синтаксисом языка Go, настоятельно рекомендую прочитать Go by Example. Отличительные черты языка:

  • Строгая статическая типизация (утиная типизация в случае интерфейсов);
  • Полноценная поддержка юникода;
  • Java/Python/Haskell-подобный сборщик мусора;
  • Прочие фишки функционального программирования — лямбды, замыкания и тп;
  • В Go есть указатели, но над ними нельзя выполнять арифметические операции, как в C/C++/D;
  • Так называемые goroutines — легковесные потоки, для управления которыми и взаимодействия между которыми в Go предусмотрены удобные средства;
  • Отсутствие ООП-фанатизма, по большому счету Go является процедурным языком с поддержкой интерфейсов;
  • Нет поддержки исключений, вместо них предлагается использовать интерфейс error и возможность функций возвращать несколько значений;
  • Язык является интерпретируемым и компилируемым, при разработке Go особое внимание уделялось скорости компиляции;
  • Разработан в Google, проектированием занимались Кен Томпсон, Роб Пайк и Роберт Гризмер, если эти имена вам о чем-то говорят;

Установка Go под Debian:

sudo apt-get install golang

Под FreeBSD существует бинарный пакет, но он довольно старый. Устанавливаться из портов Go отказался, дескать «go-1.0.1,1 is only for amd64, while you are running i386» 0_o. Однако установка в соответствии с инструкцией на сайте проекта прошла без проблем.

Запуск интерпретатора:

go run test.go

Компиляция:

go build test.go

В качестве IDE я использовал свой любимый VIM. Подсветка синтаксиса настраивается так:

mkdir -p ~/.vim/syntax/
cd ~/.vim/syntax/
wget http://go.googlecode.com/hg-history/release/misc/vim/syntax/go.vim
mkdir -p ../ftdetect/
echo 'au BufRead,BufNewFile *.go set filetype=go' > ../ftdetect/go.vim

Вроде, все. Как там обстоят дела под Windows — не ведаю.

Теперь напишем немного кода

Рассмотрим задачу из программерского конкурса от Darkus’а за июнь. Как правило, при изучении нового языка программирования, сначала я пишу прогу на уже знакомом мне языке, в роли которого обычно выступает Perl:

#!/usr/bin/perl

use strict;
use warnings;

my %cubes;
my $min_dist = 999_999_999;
my @min_pair = (undef, undef);
my $infile = shift;

my $nbits = 9;

open my $fid, $infile or die $!;
my $nline = 0;
while(<$fid>) {
  s/(^\s+|\s+$)//gs;
  my ($x,$y,$z) = split /\s+/;
  my @compare = ();
  my ($cx, $cy, $cz) = ( ($x >> $nbits),
                         ($y >> $nbits),
                         ($z >> $nbits) );
  for my $dx( (-1, 0, 1) ) {
    for my $dy( (-1, 0, 1) ) {
      for my $dz( (-1, 0, 1) ) {
        my $ncube = ($cx + $dx).'.'.($cy + $dy).'.'.($cz + $dz);
        next unless defined $cubes{$ncube};
        push @compare, $_ for(@{$cubes{$ncube}});
      }
    }
  }

  for my $curr(@compare) {
    my $dist = ($x - $curr->[0])**2 +
               ($y - $curr->[1])**2 +
               ($z - $curr->[2])**2;
    next unless $dist > 0;
    if($dist < $min_dist) {
      $min_dist = $dist;
      @min_pair = ([$x, $y, $z], $curr);
    }
  }

  push @{$cubes{$cx.'.'.$cy.'.'.$cz}}, [$x, $y, $z];

  $nline++;
  if($nline % 10000 == 0) {
    warn "parsed $nline lines, min_dist**2 = $min_dist\n";
  }
}
close $fid;
print "DONE! MIN_DIST = ".sqrt($min_dist)."\n";
print "($min_pair[0][0],$min_pair[0][1],$min_pair[0][2])\n";
print "($min_pair[1][0],$min_pair[1][1],$min_pair[1][2])\n";

… а затем переписываю ее на изучаемый язык:

package main

import (
    "github.com/glenn-brown/golang-pkg-pcre/src/pkg/pcre"
    "container/list"
    "strconv"
    "bufio"
    "math"
    "fmt"
    "os"
  )

type Point3D struct {
  x,y,z int
}

const nbits = 9

func pointToKey(point Point3D) Point3D {
  return Point3D {
      point.x >> nbits,
      point.y >> nbits,
      point.z >> nbits,
    }
}

type PointReader pcre.Regexp

func newPointReader() (pr PointReader) {
  re := pcre.MustCompile("(\\d+)\\s+(\\d+)\\s+(\\d+)", 0)
  return PointReader(re)
}

func (pr PointReader) readPoint(str string) (
    rslt Point3D, err error) {
  m := pcre.Regexp(pr).MatcherString(str,0)
  if !m.Matches() {
    err = fmt.Errorf("Failed to parse '%v'\n", str)
    return
  }

  x, _ := strconv.Atoi(string(m.Group(1)))
  y, _ := strconv.Atoi(string(m.Group(2)))
  z, _ := strconv.Atoi(string(m.Group(3)))
  rslt = Point3D{x,y,z}
  return
}

func pow2(x int) int {
  return x * x;
}

func main() {
  if len(os.Args) < 2 {
    fmt.Fprintf(os.Stderr, "Usage:\n%v [infile]\n", os.Args[0])
    return
  }

  fid, ferr := os.Open(os.Args[1])
  if ferr != nil {
    return
  }

  cubes := make(map[Point3D]*list.List);
  reader := bufio.NewReader(fid);
  minDist := 999999999
  minPair := make([]Point3D, 2)
  nline := 0;
  pr := newPointReader()
  for {
    line, rerr := reader.ReadString('\n')
    if rerr != nil { break }

    point, perr := pr.readPoint(line)
    if perr != nil {
      fid.Close()
      return
    }

    compare := list.New()
    initKey := pointToKey(point)
    for dx := -1; dx <= 1; dx++ {
      for dy := -1; dy <= 1; dy++ {
        for dz := -1; dz <= 1; dz++ {
          key := initKey
          key.x += dx
          key.y += dy
          key.z += dz
          values, exists := cubes[key]
          if !exists { continue }
          compare.PushBackList(values)
        }
      }
    }

    for curr := compare.Front();curr != nil;curr = curr.Next(){
      currPoint, _ := curr.Value.(Point3D)
      dist := pow2(point.x - currPoint.x) +
              pow2(point.y - currPoint.y) +
              pow2(point.z - currPoint.z)
      if dist == 0 { continue }
      if dist < minDist {
        minPair[0] = point
        minPair[1] = currPoint
        minDist = dist
      }
    }

    value, exists := cubes[initKey]
    if !exists {
      value = list.New()
      cubes[initKey] = value
    }
    value.PushBack(point)

    nline++;
    if nline % 10000 == 0 {
      fmt.Printf("Parsed %v lines, minDist**2 = %v\n",
        nline, minDist)
    }
  }
  fid.Close()

  fmt.Printf("MIN_DIST = %v, minPair = %v\n",
    math.Sqrt(float64(minDist)), minPair);
}

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

Вы, конечно же, не могли не обратить внимание на использование в данной программе библиотеки PCRE. Разумеется, программа во время своей работы ничего не скачивает с гитхаба, просто таким образом в Go разрешаются конфликты в именах библиотек. Установка упомянутой библиотеки под Debian производится следующим образом:

sudo apt-get install libpcre++-dev
sudo go get github.com/glenn-brown/golang-pkg-pcre/src/pkg/pcre

Я был вынужден использовать PCRE, потому что стандартная библиотека Go для работы с регулярными выражениями полностью написана на самом Go. В связи с этим, ее производительность не то, чтобы ни на что не годилась, но все же заметно уступает PCRE.

Мой первый вариант программы на Go решал поставленную задачу за 225 секунд, используя 315 Мб оперативной памяти, причем Perl-скрипту требовалось всего лишь 100 секунд и на каких-то 50 Мб больше оперативки. Поначалу я был очень расстроен. Но разобравшись в проблеме и произведя соответствующие оптимизации (использование PCRE, кэширование скомпилированных регулярных выражений и некоторые другие), я сотворил программу, которую привел выше. На моем средненьком, по нынешним меркам, компьютере она справляется с задачей за 28 секунд и использует 177 Мб оперативной памяти.

Существуют и другие свидетельства явного превосходства Go в плане производительности не только над интерпретируемыми, но и некоторыми компилируемыми языками программирования:

Бенчмарк с участием Google Go

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

Дополнение: По состоянию на 2015-ый год язык Go очень здорово разогнали, теперь по скорости он сравним с Java и Haskell. Кроме того, в нем больше не возникает упомянутых выше «подвисаний».

Мой вердикт следующий…

После многолетнего опыта работы с Perl, язык Go кажется мне слишком многословным. Сравните приведенные выше исходники или попробуйте написать на Go аналог следующего кода:

  print "$_ => $hash{$_}\n"
    for sort { $hash{$a} <=> $hash{$b} } keys %hash;

Но во всем остальном он очень даже неплох. Go прост в изучении и имеет широкую область применения. Язык представляет собой неплохой компромисс между выразительностью скриптовых и производительностью компилируемых языков. Хотя баланс чуть сильнее сдвинут в сторону Си, чем я ожидал.

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

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

Дополнение: Также вас могут заинтересовать посты Некоторые подводные грабли в языке Go и Некоторые тонкости управления зависимостями в Go.

Метки: , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.