пятница, 22 ноября 2019 г.

Golang puzzlers: синхронизации go-процедур (goroutine)

В этом посте рассмотрим задачу синхронизации go-процедур (goroutine). Дан код:

package main

import (
  "fmt"
)

func main() {

  hosts := []string{"ahost", "bhost", "chost"}

  for i : range hosts {
    go func(){
      fmt.Println(hosts[i])
    }()
  }
}

Вопрос: что выведет данный код? Ответ: он вообще не скомпилируется. Это видно невооруженным взглядом - здесь банальная ошибка в синтаксисе range - range для среза будет возвращать 2 значения - индекс элемента и содержимое элемента, а также оператор присваивания написан неверно - вместо := написано :. Хорошо, исправим:

package main

import (
  "fmt"
)

func main() {

  hosts := []string{"ahost", "bhost", "chost"}

  for i, _ := range hosts {
    go func(){
      fmt.Println(hosts[i])
    }()
  }
}

Вопрос: что выведет код сейчас? Ответ: ничего. Почему? Потому что запустив 3 goroutine основной поток исполнения функции main не будет ждать их выполнения и выполнит возврат, поэтому в консоль не будет ничего напечатано. Необходимо чтобы были напечатаны все три значения hosts - по одному на каждую goroutine. Попробуем исправить: здесь у нас появляется выбор, поскольку получить такой результат можно разными способами. Рассмотрим использование sync.WaitGroup:

package main

import (
  "fmt"
  "sync"
)

func main() {

  var wg sync.WaitGroup

  hosts := []string{"ahost", "bhost", "chost"}

  for i, _ := range hosts {
    wg.Add(1)
    go func() {
      fmt.Println(hosts[i])
      wg.Done()
    }()
  }
  wg.Wait()
}

Вопрос: что выведет код сейчас? Ответ: три раза chost. Почему? Потому что каждая goroutine, хотя и является замыканием и может использовать переменную i, определенную вне анонимной функции, запускаемой как go-процедура (goroutine), но не запоминает значения i, а на момент исполнения каждой из go-процедур цикл range уже завершился и в i сохранилось последнее значение 2, поэтому все go-процедуры печатают значение hosts[2]. Исправим, чтобы получить значение i, которое было во время запуска go-процедуры:

package main

import (
  "fmt"
  "sync"
)

func main() {
  var wg sync.WaitGroup

  hosts := []string{"ahost", "bhost", "chost"}

  for i, _ := range hosts {
    wg.Add(1)
    go func(i int) {
      fmt.Println(hosts[i])
      wg.Done()
    }(i)
  }
  wg.Wait()
}

Теперь будут выведены все 3 значения hosts. go-процедуры будут выполняться конкурентно, но порядок вывода не гарантируется. Похожий на первый взгляд результат мы можем получить используя для синхронизации небуферизованный канал:

package main

import (
  "fmt"
)

func main() {

  // Создаем небуферизованный канал
  ch := make(chan int)

  hosts := []string{"ahost", "bhost", "chost"}

  for i, _ := range hosts {
    go func(i int) {
      fmt.Println(hosts[i])
      // Получаем сообщение из канала
      <-ch
    }(i)
    // Отправляем 0 в канал
    ch <- 0
  }

}

Теперь также будут выведены все 3 значения hosts. За счет того что прием из небуферизованного канала происходит до завершения отправки по небуферизованному каналу мы гарантируем что последняя go-процедура будет выполнена и все значения будут напечатаны.

Но у данного подхода есть проблема - мы потеряли конкурентность в исполнении go-процедур - теперь они запускаются последовательно и каждая следующая go-процедура, ждет завершения предыдущей, поскольку в range цикле запись в небуферизованный канал может быть выполнена только когда канал пуст. Можно вернуть конкурентность, для этого используем буферизованный канал с размером равным размеру hosts - в данному случае кроме конкурентности мы сможем также получить сохранение последовательности вывода в консоль значений hosts, поскольку значения i будут записаны в канал в порядке их итерации. Для ожидания завершения всех go-процедур используем другой буферизованный канал out размером равным размеру hosts. С помощью утверждения select считываем из него значения и подсчитываем количество вернувшихся значений либо ожидаем 1 мс и снова проверяем содержимое канала:

package main

import (
  "fmt"
  "time"
)

func main() {

  hosts := []string{"ahost", "bhost", "chost"}
  size := len(hosts)

  ch := make(chan int, size)
  out := make(chan int, size)

  for i, _ := range hosts {
    ch <- i
    go func() {
      fmt.Println(hosts[<-ch])
      out <- 0
    }()
  }
  var count int
  for {
    select {
    case <-out:
      count++
      if count == size {
        return
      }
    default:
      time.Sleep(1 * time.Millisecond)
    }
  }
}

Теперь мы получили то, что предполагал данный пример изначально: конкурентное исполнение go-процедур с сохранением порядка запуска.

Запустить пример в песочнице play.golang.org


Читайте также:


четверг, 21 ноября 2019 г.

Golang puzzlers: длина строки

Пример этого поста: определение длины строки. Дан код:

package main

import "fmt"

func main() {
  test := "frйday"
  fmt.Println(len(test))
}

Вопрос: что выведется при запуске?

На первый взгляд покажется что ответ должен быть 6, поскольку в строке 6 символов. Но при запуске в выводе будет 7!

В чем подвох? Дело в том что len измеряет строку по байтам, а не по символам. 5 символов frday являются ASCII символами и в UTF-8 (а исходный код всех строк в Go представлен UTF-8 текстом) каждый из этих символов занимает 1 байт, а символ й не является ASCII символом, и в UTF-8 занимает 2 байта, поэтому 5+2=7. Стоит сразу отметить, что исходный код строк в Go состоит из UTF-8 текста, но в UTF-8 если символ вмещается в 1 байт (как с ASCII символами), то он и записывается 1 байтом. Символ й занимает 2 байта, но если бы мы взяли какой-нибудь китайский иероглиф, который в UTF-8 занимает 3 байта, в нашем примере, то могли бы получить даже 8 в ответе, например:

package main

import "fmt"

func main() {
  test := "fr語day"
  fmt.Println(len(test))
}

Вывод:

8

Запустить пример в песочнице play.golang.org

Для более подробной информации о строках, символах и кодировке читайте этот пост.


Читайте также:


Golang puzzlers: возврат ошибки

В постах Golang puzzlers мы рассмотрим различные случаи в Golang, которые могут быть неочевидными на первый взгляд.

Пример этого поста. Дан код:

package main

import (
 "fmt"
)

type myType struct {
 msg string
}

func (m *myType) Error() string {
 return m.msg
}

func test() *myType {
 return nil
}

func main() {
 var err error
 err = test()
 if err != nil {
  fmt.Println("occured error")
  return
 }
 fmt.Println("ok")
}

Вопрос: что выведет данный код?

На первый взгляд все очевидно: задан тип myType, указатель на который реализует интерфейс error. Раз указатель на myType реализует интерфейс ошибки, то кажется что все верно - функция test возвращает указатель на myType, а в функции main возвращаемое значение будет записано в переменную типа error. При запуске программа скомпилируется и кажется, что раз мы возвращаем nil в test, то будет выведено ok, но в консоль выведется occured error!

Начнем разбор - распечатаем переменную err, которая должна быть равна nil:

package main

import (
 "fmt"
)

type myType struct {
 msg string
}

func (m *myType) Error() string {
 return m.msg
}

func test() *myType {
 return nil
}

func main() {
 var err error
 err = test()
 if err != nil {
  fmt.Println("occured error")
  fmt.Println(err)
  return
 }
 fmt.Println("ok")
}

При запуске получим:

occured error
<nil>

Значит err равно nil? В чем подвох? Мы сможем понять это использовав пакет reflect для разбора:

package main

import (
 "fmt"
 "reflect"
)

type myType struct {
 msg string
}

func (m *myType) Error() string {
 return m.msg
}

func test() *myType {
 return nil
}

func main() {
 var err error
 err = test()
 if err != nil {
  fmt.Println("error")
  fmt.Println(err)
  e := reflect.ValueOf(err)
  fmt.Println(e)
  fmt.Println(e.Kind())
  return
 }
 fmt.Println("ok")
 fmt.Println(err)
}

При запуске получим:

error
<nil>
<nil>
ptr

Что получается? В err записывается указатель со значением nil. Это указатель на тип myType, который реализует интерфейс error, поэтому этот указатель можно записать в переменную типа error - компилятор позволит нам это. Но при проверке на nil за nil засчитается только значение записанное по типу интерфейса error - то есть если бы функция test в качестве возвращаемого значения в сигнатуре функции указывала бы интерфейс error, тогда все бы работало как предполагается и в консоль было бы выведено ok.

package main

import (
 "fmt"
)

type myType struct {
 msg string
}

func (m *myType) Error() string {
 return m.msg
}

func test() error {
 return nil
}

func main() {
 var err error
 err = test()
 if err != nil {
  fmt.Println("error")
  return
 }
 fmt.Println("ok")
}

При запуске получим:

ok

Запустить пример в песочнице play.golang.org


Читайте также:


четверг, 14 ноября 2019 г.

Работа с JSON в Golang

JSON (JavaScript Object Notation) - это простой формат обмена данными. Синтаксически это напоминает объекты и списки JavaScript. Он чаще всего используется для связи между веб-интерфейсами и программами JavaScript, работающими в браузере, но также используется и во многих других местах. Его домашняя страница, json.org, предоставляет четкое и краткое определение стандарта.

С пакетом json легко и быстро читать и записывать данные JSON из ваших программ Go.

Кодирование

Для кодирования данных в JSON используется функция Marshal.

func Marshal(v interface{}) ([]byte, error)

Задав структуру данных Go, Message,

type Message struct {
    Name string
    Body string
    Time int64
}

и экземпляр сообщения

m := Message{"Alice", "Hello", 1294706395881547000}

мы можем получить JSON-кодированную версию m, используя json.Marshal:

b, err := json.Marshal(m)

Если все хорошо, err будет nil, а b будет []byte, содержащим JSON данные:

b == []byte(`{"Name":"Alice","Body":"Hello","Time":1294706395881547000}`)

Будут закодированы только структуры данных, которые могут быть представлены валидным JSON:

  • Объекты JSON поддерживают только строки в качестве ключей; чтобы кодировать Go тип map он должен иметь вид map[string]T (где T - любой тип Go, поддерживаемый пакетом json).
  • Channel, complex и func типы не могут быть закодированы.
  • Циклические структуры данных не поддерживаются; они приведут к попаданию Marshal в бесконечный цикл.
  • Указатели будут закодированы как значения, на которые они указывают (или 'null', если указатель равен nil).

Пакет json обращается только к экспортированным полям struct типов (те, которые начинаются с заглавной буквы). Поэтому в выводе JSON будут присутствовать только экспортированные поля структуры.

Декодирование

Для декодирования JSON данных используем функцию Unmarshal.

func Unmarshal(data []byte, v interface{}) error

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

var m Message

и вызвать json.Unmarshal, передав ему []byte JSON данных и указатель на m

err := json.Unmarshal(b, &m)

Если b содержит допустимый JSON, который соответствует m, после вызова err будет nil и данные из b будут сохранены в структуре m, как если бы это было сделано с помощью присваивания, подобного следующему:

m = Message{
    Name: "Alice",
    Body: "Hello",
    Time: 1294706395881547000,
}

Как Unmarshal идентифицирует поля для хранения декодированных данных? Для заданного ключа JSON "Foo" Unmarshal просматривает поля структуры назначения, чтобы найти (в порядке предпочтения):

  • Экспортированное поле с тегом "Foo"
  • Экспортированное поле с именем "Foo"
  • Экспортированное поле с именем "FOO" или "FoO" или другое нечувствительное к регистру совпадение "Foo"

Что происходит, когда структура данных JSON не совсем соответствует типу Go?

b := []byte(`{"Name":"Bob","Food":"Pickle"}`)
var m Message
err := json.Unmarshal(b, &m)

Unmarshal будет декодировать только те поля, которые он может найти в типе назначения. В этом случае будет заполнено только поле Name в m, а поле Food будет игнорироваться. Это поведение особенно полезно, когда вы хотите выбрать только несколько определенных полей из большого JSON-объекта. Это также означает, что Unmarshal не затронет любые неэкспортированные поля в структуре назначения.

Но что, если вы заранее не знаете структуру данных JSON?

Универсальный JSON с interface{}

Тип interface{} (пустой интерфейс) описывает интерфейс с нулевыми методами. Каждый тип Go реализует как минимум ноль методов и поэтому удовлетворяет пустому интерфейсу.

Пустой интерфейс служит общим типом контейнера:

var i interface{}
i = "a string"
i = 2011
i = 2.777

Утверждение типа обращается к подлежащему конкретному типу:

r := i.(float64)
fmt.Println("the circle's area", math.Pi*r*r)

Или, если базовый тип неизвестен, переключатель типа определяет тип:

switch v := i.(type) {
case int:
    fmt.Println("twice i is", v*2)
case float64:
    fmt.Println("the reciprocal of i is", 1/v)
case string:
    h := len(v) / 2
    fmt.Println("i swapped by halves is", v[h:]+v[:h])
default:
    // i ни один из типов выше
}

Пакет json использует значения map[string]interface{} и []interface{} для хранения произвольных объектов и массивов JSON; он с радостью разархивирует любой допустимый большой бинарный объект JSON (blob) в простое значение interface{}. Конкретные типы Go по умолчанию:

  • bool для логических выражений JSON
  • float64 для JSON чисел
  • string для JSON строк
  • nil для JSON null

Декодирование произвольных данных

Рассмотрим JSON данные, хранящиеся в переменной b:

b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`)

Не зная структуры этих данных, мы можем декодировать их в значение interface{} с помощью Unmarshal:

var f interface{}
err := json.Unmarshal(b, &f)

В этот момент значением Go в f будет карта, ключи которой являются строками, а сами значения хранятся как пустые значения интерфейса:

f = map[string]interface{}{
    "Name": "Wednesday",
    "Age":  6,
    "Parents": []interface{}{
        "Gomez",
        "Morticia",
    },
}

Чтобы получить доступ к этим данным, мы можем использовать утверждение типа для доступа к подлежащему map[string]interface{}:

m := f.(map[string]interface{})

Затем мы можем перебрать карту с помощью оператора range и использовать переключатель типа для доступа к ее значениям в качестве их конкретных типов:

for k, v := range m {
    switch vv := v.(type) {
    case string:
        fmt.Println(k, "is string", vv)
    case float64:
        fmt.Println(k, "is float64", vv)
    case []interface{}:
        fmt.Println(k, "is an array:")
        for i, u := range vv {
            fmt.Println(i, u)
        }
    default:
        fmt.Println(k, "is of a type I don't know how to handle")
    }
}

Таким образом, вы можете работать с неизвестными JSON данными, сохраняя при этом преимущества безопасности типов.

Ссылочные типы

Определим тип Go, который будет содержать данные из предыдущего примера:

type FamilyMember struct {
    Name    string
    Age     int
    Parents []string
}

var m FamilyMember
err := json.Unmarshal(b, &m)

Демонстрация этих данных в значение FamilyMember работает, как и ожидалось, но если мы посмотрим внимательнее, то увидим, что произошла замечательная вещь. С помощью оператора var мы выделили структуру FamilyMember, а затем предоставили указатель на это значение для Unmarshal, но в то время поле Parents было нулевым значением среза. Чтобы заполнить поле Parents, Unmarshal выделил новый срез за кулисами. Это типично для того, как Unmarshal работает с поддерживаемыми ссылочными типами (указатели, срезы и карты).

Рассмотрим конвертирование в эту структуру данных:

type Foo struct {
    Bar *Bar
}

Если бы в объекте JSON было поле Bar, Unmarshal выделил бы новый Bar и заполнил его. Если нет, Bar будет оставлен как нулевой указатель.

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

type IncomingMessage struct {
    Cmd *Command
    Msg *Message
}

и отправляющая сторона может заполнить поле Cmd и/или поле Msg объекта JSON верхнего уровня, в зависимости от типа сообщения, которое они хотят передать. Unmarshal при декодировании JSON в структуру IncomingMessage будет выделять только структуры данных, присутствующие в JSON данных. Чтобы узнать, какие сообщения обрабатывать, программисту просто нужно проверить, что Cmd или Msg не ноль.

Потоковые кодировщики и декодеры

Пакет json предоставляет типы Decoder и Encoder для поддержки обычной операции чтения и записи потоков данных JSON. Функции NewDecoder и NewEncoder оборачивают типы интерфейсов io.Reader и io.Writer.

func NewDecoder(r io.Reader) *Decoder
func NewEncoder(w io.Writer) *Encoder

Вот пример программы, которая читает серию объектов JSON из стандартного ввода, удаляет все, кроме поля Name, из каждого объекта, а затем записывает объекты в стандартный вывод:

package main

import (
    "encoding/json"
    "log"
    "os"
)

func main() {
    dec := json.NewDecoder(os.Stdin)
    enc := json.NewEncoder(os.Stdout)
    for {
        var v map[string]interface{}
        if err := dec.Decode(&v); err != nil {
            log.Println(err)
            return
        }
        for k := range v {
            if k != "Name" {
                delete(v, k)
            }
        }
        if err := enc.Encode(&v); err != nil {
            log.Println(err)
        }
    }
}

Из-за повсеместного распространения программ чтения и записи эти типы кодировщиков и декодеров могут использоваться в широком диапазоне сценариев, таких как чтение и запись в соединения HTTP, веб-сокеты или файлы.

Пример использования тегов в структуре

Использование тегов в структуре кодируемой в JSON позволяет получить названия полей в результирующем JSON, отличающиеся от названия полей в структуре. В следующем примере в результирующем JSON поле BrandID будет выглядеть как brand_id:

package main

import (
 "encoding/json"
 "fmt"
)

type Item struct {
 ID      uint   `json:"id"`
 Title   string `json:"title"`
 BrandID uint   `json:"brand_id"`
}

func main() {

 item := Item{ID: 1, Title: "Car", BrandID: 1}
 jitem, err := json.Marshal(item)
 if err != nil {
  fmt.Println(err.Error())
  return
 }
 fmt.Println(string(jitem))
}

Вывод:

{"id":1,"title":"Car","brand_id":1}

Запустить пример в песочнице play.golang.org


Читайте также:


понедельник, 11 ноября 2019 г.

Паттерны в Golang: Fan-Out

Fan-Out - это шаблон обмена сообщениями, используемый для распределения работы между работниками.

Можно смоделировать Fan-Out, используя каналы Go.

// Разделить канал на n каналов, 
// которые получают сообщения в циклическом порядке.
func Split(ch <-chan int, n int) []<-chan int {

  // Создаем пул из n каналов
  cs := make([]chan int)
  for i := 0; i < n; i++ {
    cs = append(cs, make(chan int))
  }

  // Распределяет работу в круговом порядке 
  // среди указанного числа каналов, 
  // пока основной канал не будет закрыт. 
  // При закрытии основного канала закрывает 
  // все каналы и возвращается.
  toChannels := func(ch <-chan int, cs []chan<- int) {

    // Закрываем каждый канал, 
    // когда выполнение заканчивается.
    defer func(cs []chan<- int) {
      for _, c := range cs {
        close(c)
      }
    }(cs)

    // Направляем сообщения из
    // основного канала ch
    // в каналы из пула cs
    for {
      for _, c := range cs {
        select {
        case val, ok := <-ch:
          if !ok {
            return
          }

          c <- val
        }
      }
    }
  }

  go toChannels(ch, cs)

  return cs
}

Функция `Split` преобразует один канал в список каналов с помощью goroutine для копирования полученных значений по каналам в списке в циклическом порядке.


Читайте также:


среда, 6 ноября 2019 г.

Паттерны в Golang: Fan-In

Fan-In - это паттерн обмена сообщениями, используемый для создания воронки для работы среди исполнителей. Источником сообщений могут быть клиенты, а место назначения - сервер.

Мы можем смоделировать Fan-In паттерн используя каналы Go.

// Объединяем разные каналы в один канал
func Merge(cs ...<-chan int) <-chan int {
  var wg sync.WaitGroup

  out := make(chan int)

  // Запускаем send goroutine 
  // для каждого входящего канала в cs. 
  // send копирует значения из c в out 
  // до тех пор пока c не закрыт, затем вызываем wg.Done.
  send := func(c <-chan int) {
    for n := range c {
      out <- n
    }
    wg.Done()
  }

  wg.Add(len(cs))
  for _, c := range cs {
    go send(c)
  }

  // Запускаем goroutine чтобы закрыть out 
  // когда все send goroutine выполнены
  // Это должно начаться после вызова wg.Add.
  go func() {
    wg.Wait()
    close(out)
  }()
  return out
}

Функция `Merge` преобразует список каналов в один канал, запуская goroutine для каждого входящего канала, которая копирует значения в единственный исходящий канал.

Как только все send goroutine были запущены, запускается отдельная goroutine с функцией, ожидающей завершения всех запущенных, чтобы закрыть основной канал.


Читайте также:


понедельник, 4 ноября 2019 г.

Golang в Google: дизайн языка на службе разработки программ, часть 3

Rob Pike, 2012

Продолжение, начало в части 1 и части 2.

9. Синтаксис

Синтаксис - это пользовательский интерфейс языка программирования. Хотя это оказывает ограниченное влияние на семантику языка, который, возможно, является наиболее важным компонентом, синтаксис определяет читабельность и, следовательно, ясность языка. Кроме того, синтаксис имеет решающее значение для инструментов: если язык трудно анализировать, автоматизированные инструменты трудно писать.

Таким образом, Go был разработан с ясностью и удобством и имеет чистый синтаксис. По сравнению с другими языками в семействе C его грамматика скромна по размеру: всего 25 ключевых слов (C99 - 37; C++ 11 - 84; цифры продолжают расти). Что еще более важно, грамматика является правильной и поэтому легко разбирается (в основном; есть несколько причуд, которые мы могли бы исправить, но не обнаружили достаточно рано). В отличие от C и Java и особенно C++, Go может быть проанализирован без информации о типе или таблицы символов; нет конкретного типа контекста. Грамматику легко разъяснить, и поэтому инструменты легко писать.

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

var fn func([]int) int
type T struct { a, b int }

по сравнению с C

int (*fn)(int[]);
struct T { int a, b; }

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

var buf *bytes.Buffer = bytes.NewBuffer(x) // явное
buf := bytes.NewBuffer(x)                  // производное

Синтаксис функции прост для простых функций. В этом примере объявляется функция Abs, которая принимает одну переменную x типа T и возвращает единственное значение float64:

func Abs(x T) float64

Метод - это просто функция со специальным параметром, его получателем, который можно передать функции, используя стандартную запись "точка". Синтаксис объявления метода помещает получателя в скобки перед именем функции. Вот та же самая функция, теперь как метод типа T:

func (x T) Abs() float64

А вот переменная (замыкание) с аргументом типа T; Go имеет функции первого класса и замыкания:

negAbs := func(x T) float64 { return -Abs(x) }

Наконец, в Go функции могут возвращать несколько значений. Распространенным случаем является возврат результата функции и значения ошибки в виде пары, например:

func ReadByte() (c byte, err error)

c, err := ReadByte()
if err != nil { ... }

Мы поговорим об ошибках позже.

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

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

10. Именование

Go использует необычный подход к определению видимости идентификатора, возможность для клиента пакета использовать элемент, названный идентификатором. В отличие, например, от private и public ключевых слов, в Go само имя содержит информацию: регистр начальной буквы идентификатора определяет видимость. Если начальный символ является заглавной буквой, идентификатор экспортируется (общедоступный); в противном случае это не так:

  • заглавная буква: Name видно клиентам пакета
  • в противном случае: name (или _Name) не видно клиентам пакета

Это правило распространяется на переменные, типы, функции, методы, константы, поля - на все. Это все, что нужно сделать.

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

Результат, опять же, ясность: исходный текст программы просто выражает значение программиста.

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

  • всеобъемлящая (universe) (предварительно объявленные идентификаторы, такие как int и string)
  • пакет (все исходные файлы пакета находятся в одной области видимости)
  • файл (только для импорта пакетов переименовывает; на практике это не очень важно)
  • функция (обычная)
  • блок (обычный)

Нет пространства для пространства имен, класса или другой оберточной конструкции. Имена происходят из очень немногих мест в Go, и все имена соответствуют одной и той же иерархии областей видимости: в любом заданном месте в исходном коде идентификатор обозначает ровно один языковой объект, независимо от того, как он используется. (Единственное исключение - метки утверждений, цели утверждений break и т.п.; они всегда имеют область видимость функции.)

Это имеет последствия для ясности. Обратите внимание, например, что методы объявляют явного получателя и что он должен использоваться для доступа к полям и методам типа. Там нет неявного this. То есть всегда пишут

rcvr.Field

(где rcvr - это любое имя, выбранное для переменной-получателя), поэтому все элементы типа всегда отображаются лексически связанными со значением типа-получателя. Точно так же квалификатор пакета всегда присутствует для импортированных имен; всегда пишут io.Reader, а не Reader. Это не только ясно, но и освобождает идентификатор Reader как полезное имя для использования в любом пакете. На самом деле в стандартной библиотеке есть несколько экспортируемых идентификаторов с именем Reader или Printf, но какой из них упоминается, всегда однозначно.

Наконец, эти правила объединяются, чтобы гарантировать, что кроме предопределенных имен верхнего уровня, таких как int, (первый компонент) каждое имя всегда объявляется в текущем пакете.

Кратко, имена локальные. В C, C++ или Java имя y может относиться к чему угодно. В Go y (или даже Y) всегда определяется внутри пакета, в то время как интерпретация x.Y ясна: найдите x локально, Y принадлежит ему.

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

Следует упомянуть еще один аспект именования: поиск метода всегда осуществляется только по имени, а не по сигнатуре (типу) метода. Другими словами, у одного типа никогда не может быть двух методов с одинаковым именем. Учитывая метод x.M, существует только один M, связанный с x. Опять же, это позволяет легко определить, к какому методу относится только имя. Это также упрощает реализацию вызова метода.

11. Семантика

Семантика операторов Go обычно C-подобна. Это компилируемый, статически типизированный, процедурный язык с указателями и так далее. По замыслу он должен чувствовать себя знакомым программистам, привыкшим к языкам в семействе С. При запуске нового языка важно, чтобы целевая аудитория могла быстро его выучить; укоренение Go в семействе C помогает молодым программистам, большинство из которых знают Java, JavaScript и, возможно, C, находить Go легким в освоении.

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

  • нет арифметики указателей
  • нет неявных числовых преобразований
  • границы массива всегда проверяются
  • нет псевдонимов типов (после типа X int, X и int являются разными типами, а не псевдонимами)
  • ++ и -- являются утверждениями, а не выражениями
  • присваивание не является выражением
  • законно (даже рекомендуется) брать адрес стековой переменной
  • и многое другое

Существуют и более значительные изменения, далеко идущие от традиционных моделей C, C++ и даже Java. К ним относятся лингвистическая поддержка для:

  • конкурентности
  • сборки мусора
  • типов интерфейсов
  • отражения (reflection)
  • переключателей типа

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


Читайте также:


купить игрушку gopher