One place for hosting & domains

      Archives hassan latif

      Видимость пакетов в Go


      Введение

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

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

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

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

      Предварительные требования

      Для выполнения примеров из этой статьи вам потребуется следующее:

      .
      ├── bin
      │
      └── src
          └── github.com
              └── gopherguides
      

      Экспортированные и неэкспортированные элементы

      В отличие от таких языков программирования, как Java и Python, где используются различные* модификаторы доступа*, декларирующие элементы как public, private или protected. Декларирование помогает Go определить, являются ли элементы экспортированными или неэкспортированными. В этом случае экспорт элемента делает его видимым за пределами текущего пакета. Если элемент не экспортирован, его можно видеть и использовать только внутри пакета, где он определен.

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

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

      greet.go

      package greet
      
      import "fmt"
      
      var Greeting string
      
      func Hello(name string) string {
          return fmt.Sprintf(Greeting, name)
      }
      

      Этот код декларирует, что он содержится в пакете greet. Затем он декларирует два символа: переменную Greeting и функцию Hello. Поскольку они начинаются с заглавной буквы, они являются экспортируемыми и доступны любой внешней программе. Как уже говорилось выше, создание пакета с ограничением доступа позволит лучше проектировать API и упростит внутреннее обновление пакета без нарушения работы кода, зависящего от вашего пакета.

      Определение видимости пакета

      Чтобы лучше изучить видимость пакетов в программе, мы создадим пакет logging, учитывая при этом, что мы хотим сделать видимым вне пакета, а что хотим оставить невидимым. Этот пакет logging будет отвечать за регистрацию любых сообщений нашей программы на консоли. Также он будет проверять уровень регистрации. Уровень описывает тип журнала регистрации и будет иметь одно из трех состояний: info, warning или error.

      Создайте в каталоге src каталог с именем logging, где будут размещены наши файлы регистрации:

      Перейдите в этот каталог:

      Используйте nano или другой редактор для создания файла logging.go:

      Поместите следующий код в созданный нами файл logging.go:

      logging/logging.go

      package logging
      
      import (
          "fmt"
          "time"
      )
      
      var debug bool
      
      func Debug(b bool) {
          debug = b
      }
      
      func Log(statement string) {
          if !debug {
              return
          }
      
          fmt.Printf("%s %sn", time.Now().Format(time.RFC3339), statement)
      }
      

      В первой строчке этого кода декларируется пакет с именем logging. В этом пакете содержится две экспортируемые функции: Debug и Log. Эти функции сможет вызывать любой другой пакет, импортирующий пакет logging. Также существует private переменная с именем debug. Эта переменная доступна только из пакета logging. Важно отметить, что функция Debug и переменная debug имеют одинаковое написание, но имя функции начинается с заглавной буквы, а имя переменной — нет. Это обеспечивает отдельное декларирование с разной сферой действия.

      Сохраните и закройте файл.

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

      Выйдем из каталога logging, создадим новый каталог cmd и перейдем в этот новый каталог:

      Создайте файл с именем main.go в каталоге cmd, который мы только что создали:

      Теперь мы можем добавить следующий код:

      cmd/main.go

      package main
      
      import "github.com/gopherguides/logging"
      
      func main() {
          logging.Debug(true)
      
          logging.Log("This is a debug statement...")
      }
      

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

      Откройте следующий файл go.mod в каталоге cmd:

      Затем поместите в файл следующий код:

      go.mod

      module github.com/gopherguides/cmd
      
      replace github.com/gopherguides/logging => ../logging
      

      Первая строчка этого файла сообщает компилятору, что пакет cmd имеет файловый путь github.com/gopherguides/cmd. Вторая строка сообщает компилятору, что каталог github.com/gopherguides/logging можно найти на локальном диске в каталоге ../logging.

      Также нам потребуется файл go.mod для нашего пакета logging. Вернемся в каталог logging и создадим файл go.mod:

      • cd ../logging
      • nano go.mod

      Добавьте в файл следующие строчки:

      go.mod

      module github.com/gopherguides/logging
      

      Это показывает компилятору, что созданный нами пакет logging на самом деле является пакетом github.com/gopherguides/logging. Это позволяет импортировать пакет в наш пакет main, используя следующую строчку, которую мы написали ранее:

      cmd/main.go

      package main
      
      import "github.com/gopherguides/logging"
      
      func main() {
          logging.Debug(true)
      
          logging.Log("This is a debug statement...")
      }
      

      Теперь у вас должны быть следующая структура каталогов и расположение файлов:

      ├── cmd
      │   ├── go.mod
      │   └── main.go
      └── logging
          ├── go.mod
          └── logging.go
      

      Мы завершили настройку и теперь можем запустить программу main из пакета cmd с помощью следующих команд:

      Результат должен выглядеть примерно следующим образом:

      Output

      2019-08-28T11:36:09-05:00 This is a debug statement...

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

      Поскольку функции Debug и Log экспортированы из пакета logging, мы можем использовать их в нашем пакете main. Однако переменная debug в пакете logging не экспортируется. Попытка ссылки на неэкспортированную декларацию приведет к ошибке во время компиляции.

      Добавьте следующую выделенную строку в файл main.go:

      cmd/main.go

      package main
      
      import "github.com/gopherguides/logging"
      
      func main() {
          logging.Debug(true)
      
          logging.Log("This is a debug statement...")
      
          fmt.Println(logging.debug)
      }
      

      Сохраните и запустите файл. Вы получите примерно следующее сообщение об ошибке:

      Output

      . . . ./main.go:10:14: cannot refer to unexported name logging.debug

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

      Видимость в структурах

      Хотя построенная нами в предыдущем разделе схема видимости может работать для простых программ, она передает слишком много значений состояния, чтобы быть полезной в нескольких пакетах. Это связано с тем, что экспортированные переменные доступны многим пакетам, которые могут изменять переменные до конфликтующих состояний. Если разрешить подобное изменение состояния пакета, будет сложно прогнозировать поведение программы. Например, при текущей схеме один пакет может задать для переменной Debug значение true, а другой — значение false для того же самого экземпляра. Это создаст проблему, поскольку будет влиять на оба пакета, импортирующих пакет logging.

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

      Замените пакет logging на следующее, чтобы исправить код и изолировать регистратор:

      logging/logging.go

      package logging
      
      import (
          "fmt"
          "time"
      )
      
      type Logger struct {
          timeFormat string
          debug      bool
      }
      
      func New(timeFormat string, debug bool) *Logger {
          return &Logger{
              timeFormat: timeFormat,
              debug:      debug,
          }
      }
      
      func (l *Logger) Log(s string) {
          if !l.debug {
              return
          }
          fmt.Printf("%s %sn", time.Now().Format(l.timeFormat), s)
      }
      

      В этом коде мы создали структуру Logger. В этой структуре будет размещено неэкспортированное состояние, включая формат времени для вывода и значение переменной debugtrue или false. Функция New задает начальное состояние для создания регистратора, в частности формат времени и статус отладки. Она сохранит присвоенные внутренние значения в неэкспортированные переменные timeFormat и debug. Также мы создали метод Log типа Logger, который принимает выражение, которое мы хотим вывести. В методе Log содержится ссылка на переменную локального метода l для получения доступа к таким его внутренним полям, как l.timeFormat и l.debug.

      Этот подход позволит нам создавать Logger в разных пакетах и использовать его независимо от его использования другими пакетами.

      Чтобы использовать его в другом пакете, изменим cmd/main.go следующим образом:

      cmd/main.go

      package main
      
      import (
          "time"
      
          "github.com/gopherguides/logging"
      )
      
      func main() {
          logger := logging.New(time.RFC3339, true)
      
          logger.Log("This is a debug statement...")
      }
      

      При запуске этой программы будут выведены следующие результаты:

      Output

      2019-08-28T11:56:49-05:00 This is a debug statement...

      В этом коде мы создали экземпляр регистратора, вызвав экспортированную функцию New. Мы сохранили ссылку на этот экземпляр в переменной logger. Теперь мы можем вызывать logging.Log для вывода выражений.

      Если мы попытаемся сослаться на неэкспортированное поле из Logger, например, на поле timeFormat, при компиляции будет выведена ошибка. Попробуйте добавить следующую выделенную строку и запустить cmd/main.go:

      cmd/main.go

      
      package main
      
      import (
          "time"
      
          "github.com/gopherguides/logging"
      )
      
      func main() {
          logger := logging.New(time.RFC3339, true)
      
          logger.Log("This is a debug statement...")
      
          fmt.Println(logger.timeFormat)
      }
      

      Будет выведено следующее сообщение об ошибке:

      Output

      . . . cmd/main.go:14:20: logger.timeFormat undefined (cannot refer to unexported field or method timeFormat)

      Компилятор определяет, что logger.timeFormat не экспортируется, и поэтому не может быть получен из пакета logging.

      Видимость в методах

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

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

      • Уровень info, представляющий события информационного типа, сообщающие пользователю о действии, например, Program started или Email sent. Они помогают выполнять отладку и отслеживать части программы, чтобы определять ожидаемое поведение.

      • Уровень warning. Эти события определяют непредвиденные события, которые не представляют собой ошибку, например, Email failed to send, retrying. Они помогают понять, какие части программы работают не так хорошо, как мы ожидали.

      • Уровень error, означающий, что в программе возникла проблема, например, File not found. Часто это вызывает прекращение работы программы.

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

      Для добавления многоуровневой регистрации нужно внести следующие изменения в файл logging/logging.go:

      logging/logging.go

      
      package logging
      
      import (
          "fmt"
          "strings"
          "time"
      )
      
      type Logger struct {
          timeFormat string
          debug      bool
      }
      
      func New(timeFormat string, debug bool) *Logger {
          return &Logger{
              timeFormat: timeFormat,
              debug:      debug,
          }
      }
      
      func (l *Logger) Log(level string, s string) {
          level = strings.ToLower(level)
          switch level {
          case "info", "warning":
              if l.debug {
                  l.write(level, s)
              }
          default:
              l.write(level, s)
          }
      }
      
      func (l *Logger) write(level string, s string) {
          fmt.Printf("[%s] %s %sn", level, time.Now().Format(l.timeFormat), s)
      }
      

      В этом примере мы ввели новый аргумент для метода Log. Теперь мы можем передать уровень сообщения журнала. Метод Log определяет уровень сообщения. Если это сообщение типа info или warning, и если поле debug имеет значение true, выполняется запись сообщения. В противном случае, сообщение игнорируется. Если это сообщение любого другого уровня, например, error, оно будет выведено в любом случае.

      Основная логика определения необходимости вывода сообщения содержится в методе Log. Также мы представили неэкспортированный метод с именем write. Метод write фактически выполняет вывод сообщения журнала.

      Теперь мы можем использовать многоуровневую регистрацию в другом пакете, изменив cmd/main.go следующим образом:

      cmd/main.go

      package main
      
      import (
          "time"
      
          "github.com/gopherguides/logging"
      )
      
      func main() {
          logger := logging.New(time.RFC3339, true)
      
          logger.Log("info", "starting up service")
          logger.Log("warning", "no tasks found")
          logger.Log("error", "exiting: no work performed")
      
      }
      

      При запуске вы увидите следующее:

      Output

      [info] 2019-09-23T20:53:38Z starting up service [warning] 2019-09-23T20:53:38Z no tasks found [error] 2019-09-23T20:53:38Z exiting: no work performed

      В этом примере cmd/main.go успешно использует экспортированный метод Log.

      Мы можем передать уровень каждого сообщения, изменив значение debug на false:

      main.go

      package main
      
      import (
          "time"
      
          "github.com/gopherguides/logging"
      )
      
      func main() {
          logger := logging.New(time.RFC3339, false)
      
          logger.Log("info", "starting up service")
          logger.Log("warning", "no tasks found")
          logger.Log("error", "exiting: no work performed")
      
      }
      

      Теперь мы видим, что выводятся только сообщения уровня error:

      Output

      [error] 2019-08-28T13:58:52-05:00 exiting: no work performed

      Если мы попытаемся вызвать метод write из-за пределов пакета logging, мы получим ошибку компиляции:

      main.go

      package main
      
      import (
          "time"
      
          "github.com/gopherguides/logging"
      )
      
      func main() {
          logger := logging.New(time.RFC3339, true)
      
          logger.Log("info", "starting up service")
          logger.Log("warning", "no tasks found")
          logger.Log("error", "exiting: no work performed")
      
          logger.write("error", "log this message...")
      }
      

      Output

      cmd/main.go:16:8: logger.write undefined (cannot refer to unexported field or method logging.(*Logger).write)

      Когда компилятор видит, что вы пытаетесь оставить ссылку на элемент другого пакета, имя которого начинается со строчной буквы, он понимает, что это неэкспортированный элемент, и выводит сообщение об ошибке.

      Регистратор в этом обучающем руководстве показывает, как написать код, открывающий другим пакетам только те части, которые требуются. Поскольку мы контролируем, какие части пакета видимы за пределами пакета, мы можем вносить будущие изменения без воздействия на код, зависящий от нашего пакета. Например, если бы мы хотели только отключить сообщения уровня info, когда debug имеет значение false, мы могли бы провести это изменение без воздействия на любую другую часть вашего API. Мы также могли безопасно вносить изменения в сообщения журнала, добавляя дополнительную информацию, такую как каталог, откуда запускается программа.

      Заключение

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

      Дополнительную информацию о пакетах в Go можно найти в статьях Импорт пакетов в Go и Написание пакетов в Go, а также в других статьях серии по программированию на Go.



      Source link

      Указатели в Go


      Введение

      При написании программного обеспечения на Go вы создаете функции и методы. Данные передаются в эти функции в виде аргументов. Иногда функции требуется локальная копия данных, но при этом вы хотите, чтобы оригинал оставался без изменений. Например, если вы работаете в банке и у вас есть функция для вывода изменений баланса пользователя в зависимости от выбранного сберегательного плана, вам не нужно изменять баланс пользователя до выбора плана, а нужно просто использовать его в расчетах. Это называется передачей по значению, поскольку вы отправляете в функцию значение переменной, но не саму переменную.

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

      В этой статье мы расскажем о создании и использовании указателей для предоставления переменной доступа к пространству памяти.

      Определение и использование указателей

      При использовании указателя на переменную нужно понять два разных элемента синтаксиса. Первый элемент называется амперсандом (&). Если вы ставите амперсанд перед именем переменной, вы указываете, что хотите получить адрес или указатель для этой переменной. Второй элемент синтаксиса — звездочка (*) или оператор разыменовывания. При декларировании переменной указателя необходимо обеспечить соответствие имени переменной типу переменной, на которую указывает указатель, с префиксом *, примерно так:

      var myPointer *int32 = &someint
      

      При этом создается указатель myPointer на переменную int32, который инициализирует указатель с адресом someint. Указатель не содержит переменную int32, а содержит только ее адрес.

      Давайте рассмотрим указатель на строку. Следующий код декларирует значение строки и указатель на строку:

      main.go

      package main
      
      import "fmt"
      
      func main() {
          var creature string = "shark"
          var pointer *string = &creature
      
          fmt.Println("creature =", creature)
          fmt.Println("pointer =", pointer)
      }
      
      

      Запустите программу с помощью следующей команды:

      При запуске программы она выводит значение переменной, а также адрес, где хранится переменная (адрес указателя). Адрес в памяти представляет собой шестнадцатеричное число, которое не предназначено для чтения людьми. На практике адреса в памяти обычно не выводятся, и мы показываем его исключительно для иллюстрации. Поскольку каждая программа при запуске создается в собственном пространстве памяти, указатель будет разным при каждом запуске и будет отличаться от показанного здесь:

      Output

      creature = shark pointer = 0xc0000721e0

      Мы присвоим первой определяемой переменной имя creature и зададим ее равной строке со значением shark. Затем мы создадим другую переменную с именем pointer. Теперь мы зададим в качестве значения переменной pointer адрес переменной creature. Мы сохраним адрес значения в переменной, используя символ амперсанда (&). Это означает, что переменная pointer хранит адрес переменной creature, а не ее реальное значение.

      Поэтому, когда мы вывели значение pointer, мы получили значение 0xc0000721e0, которое представляет собой адрес хранения переменной creature в компьютерной памяти.

      Если вы хотите вывести значение переменной, на которую указывает переменная pointer, вам нужно разыменовать эту переменную. В следующим коде используется оператор * для снятия ссылки на переменную pointer и получения его значения:

      main.go

      
      package main
      
      import "fmt"
      
      func main() {
          var creature string = "shark"
          var pointer *string = &creature
      
          fmt.Println("creature =", creature)
          fmt.Println("pointer =", pointer)
      
          fmt.Println("*pointer =", *pointer)
      }
      

      Если вы выполните этот код, вы увидите следующие результаты:

      Output

      creature = shark pointer = 0xc000010200 *pointer = shark

      Последняя добавленная нами строка убирает ссылку на переменную pointer и выводит значение, сохраненное по этому адресу.

      Если вы хотите изменить значение, сохраненное в месте расположения переменной pointer, вы также можете использовать оператор снятия ссылки:

      main.go

      package main
      
      import "fmt"
      
      func main() {
          var creature string = "shark"
          var pointer *string = &creature
      
          fmt.Println("creature =", creature)
          fmt.Println("pointer =", pointer)
      
          fmt.Println("*pointer =", *pointer)
      
          *pointer = "jellyfish"
          fmt.Println("*pointer =", *pointer)
      }
      
      

      Запустите этот код, чтобы увидеть результаты:

      Output

      creature = shark pointer = 0xc000094040 *pointer = shark *pointer = jellyfish

      Мы зададим значение, на которое ссылается переменная pointer, добавив звездочку (*) перед именем переменной, а затем зададим новое значение jellyfish. Как видите, при выводе значения разыменованной ссылки, сейчас она задана как jellyfish.

      Возможно вы не поняли этого, но мы фактически изменили значение переменной creature. Это связано с тем, что переменная pointer фактически указывает на адрес переменной creature. Это означает, что если мы изменим значение, на которое указывает переменная pointer, мы также изменим значение переменной creature.

      main.go

      package main
      
      import "fmt"
      
      func main() {
          var creature string = "shark"
          var pointer *string = &creature
      
          fmt.Println("creature =", creature)
          fmt.Println("pointer =", pointer)
      
          fmt.Println("*pointer =", *pointer)
      
          *pointer = "jellyfish"
          fmt.Println("*pointer =", *pointer)
      
          fmt.Println("creature =", creature)
      }
      

      Результат выглядит следующим образом:

      Output

      creature = shark pointer = 0xc000010200 *pointer = shark *pointer = jellyfish creature = jellyfish

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

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

      Приемники указателя функции

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

      Решение о передаче указателя или отправке значения зависит от того, требуется ли изменить это значение. Если вы не хотите, чтобы значение изменялось, отправьте его как значение. Если вы хотите, чтобы функция, куда вы передаете переменную, могла изменять эту переменную, вы должны передавать ее как указатель.

      Чтобы увидеть разницу, вначале рассмотрим функцию, передающую аргумент по значению:

      main.go

      package main
      
      import "fmt"
      
      type Creature struct {
          Species string
      }
      
      func main() {
          var creature Creature = Creature{Species: "shark"}
      
          fmt.Printf("1) %+vn", creature)
          changeCreature(creature)
          fmt.Printf("3) %+vn", creature)
      }
      
      func changeCreature(creature Creature) {
          creature.Species = "jellyfish"
          fmt.Printf("2) %+vn", creature)
      }
      
      

      Результат выглядит следующим образом:

      Output

      1) {Species:shark} 2) {Species:jellyfish} 3) {Species:shark}

      Вначале мы создали пользовательский тип с именем Creature. Он содержит одно поле с именем Species, которое представляет собой строку. В функции main мы создали экземпляр нового типа с именем creature и задали для поля Species значение shark. Затем мы вывели переменную для отображения текущего значения, сохраненного в переменной creature.

      Далее мы вызвали changeCreature и передали копию переменной creature.

      Функция changeCreature принимает один аргумент с именем creature, который относится к ранее определенному типу Creature. Затем мы изменяем значение поля Species на jellyfish и выводим его. Обратите внимание, что в функции changeCreature значение Species теперь jellyfish, и функция выводит 2) {Species:jellyfish}. Это связано с тем, что нам разрешено изменять значение в составе функции.

      Однако когда последняя строка функции main распечатывает значение creature, значение Species сохраняется как shark. Значение осталось без изменений, потому что мы передали переменную по_ значению_. Это означает, что копия данного значения была создана в памяти и передана в функцию changeCreature. Это позволяет нам иметь функцию, которая сможет изменять любые передаваемые в нее аргументы, но не сможет влиять на переменные за пределами функции.

      Затем измените функцию changeCreature так, чтобы она принимала аргумент по ссылке. Для этого мы можем изменить тип creature на указатель, используя оператор звездочка (*). Вместо передачи creature мы передаем указатель на creature или *creature. В предыдущем примере creature представляет собой структуру, где Species имеет значение shark. *creature является указателем, а не структурой, и его значение является адресом в памяти, и именно это мы передаем в функцию changeCreature().

      main.go

      package main
      
      import "fmt"
      
      type Creature struct {
          Species string
      }
      
      func main() {
          var creature Creature = Creature{Species: "shark"}
      
          fmt.Printf("1) %+vn", creature)
          changeCreature(&creature)
          fmt.Printf("3) %+vn", creature)
      }
      
      func changeCreature(creature *Creature) {
          creature.Species = "jellyfish"
          fmt.Printf("2) %+vn", creature)
      }
      

      Запустите этот код, чтобы увидеть результат:

      Output

      1) {Species:shark} 2) &{Species:jellyfish} 3) {Species:jellyfish}

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

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

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

      Нулевые указатели

      Все переменные в Go имеют нулевое значение. Это относится и к указателям. Если вы декларировали указатель на тип, но не назначили ему значения, ему будет присвоено нулевое значение nil. nil — это способ сказать, что ничего не было инициализировано.

      В следующей программе мы определяем указатель на тип Creature, но не создаем экземпляр Creature и не назначаем его адрес переменной указателя creature. Он будет иметь значение nil, и мы не сможем ссылаться ни на какие поля или методы, определенные для типа Creature:

      main.go

      package main
      
      import "fmt"
      
      type Creature struct {
          Species string
      }
      
      func main() {
          var creature *Creature
      
          fmt.Printf("1) %+vn", creature)
          changeCreature(creature)
          fmt.Printf("3) %+vn", creature)
      }
      
      func changeCreature(creature *Creature) {
          creature.Species = "jellyfish"
          fmt.Printf("2) %+vn", creature)
      }
      

      Результат выглядит следующим образом:

      Output

      1) <nil> panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x8 pc=0x109ac86] goroutine 1 [running]: main.changeCreature(0x0) /Users/corylanou/projects/learn/src/github.com/gopherguides/learn/_training/digital-ocean/pointers/src/nil.go:18 +0x26 main.main() /Users/corylanou/projects/learn/src/github.com/gopherguides/learn/_training/digital-ocean/pointers/src/nil.go:13 +0x98 exit status 2

      При запуске программы она выводит значение переменной creature, и это значение <nil>. Затем мы можем вызвать функцию changeCreature, и когда эта функция пытается задать значение поля Species, происходит паника. Это связано с тем, что ни один экземпляр переменной фактически не был создан. Поэтому программе негде хранить значение, и происходит паника.

      Чтобы предотвратить генерацию паники в программе, при получении аргумента в качестве указателя в Go обычно следует проверить, имеет ли он значение nil, прежде чем выполнять с ним какие-либо операции.

      Этот подход обычно применяется для проверки nil:

      if someVariable == nil {
          // print an error or return from the method or fuction
      }
      

      Фактически вам нужно убедиться, что в вашу функцию или метод не передавался указатель nil. При обнаружении такого указателя обычно нужно, чтобы программа возвращалась к предыдущему шагу или выводила сообщение об ошибке передачи неверного аргумента в функцию или метод. В следующем коде демонстрируется проверка на nil:

      main.go

      package main
      
      import "fmt"
      
      type Creature struct {
          Species string
      }
      
      func main() {
          var creature *Creature
      
          fmt.Printf("1) %+vn", creature)
          changeCreature(creature)
          fmt.Printf("3) %+vn", creature)
      }
      
      func changeCreature(creature *Creature) {
          if creature == nil {
              fmt.Println("creature is nil")
              return
          }
      
          creature.Species = "jellyfish"
          fmt.Printf("2) %+vn", creature)
      }
      

      Мы добавили проверку в changeCreature, чтобы посмотреть, имеет ли аргумент creature значение nil. Если это так, программа выводит сообщение creature is nil и выходит из функции. Если это не так, программа изменяет значение поля Species. Если мы запустим программу, результат будет выглядеть следующим образом:

      Output

      1) <nil> creature is nil 3) <nil>

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

      Наконец, если мы создадим экземпляр типа Creature и назначим его переменной creature, программа изменит значение ожидаемым образом:

      main.go

      package main
      
      import "fmt"
      
      type Creature struct {
          Species string
      }
      
      func main() {
          var creature *Creature
          creature = &Creature{Species: "shark"}
      
          fmt.Printf("1) %+vn", creature)
          changeCreature(creature)
          fmt.Printf("3) %+vn", creature)
      }
      
      func changeCreature(creature *Creature) {
          if creature == nil {
              fmt.Println("creature is nil")
              return
          }
      
          creature.Species = "jellyfish"
          fmt.Printf("2) %+vn", creature)
      }
      

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

      Output

      1) &{Species:shark} 2) &{Species:jellyfish} 3) &{Species:jellyfish}

      При работе с указателями существует вероятность паники в программе. Чтобы избежать паники, следует посмотреть, имеет ли указатель значение nil, прежде чем пытаться получить доступ к любым полям или методам, которые он определяет.

      Далее мы посмотрим, как использование указателей и значений влияет на определение методов типа.

      Получатели указателей методов

      Приемник в go — это аргумент, определенный в декларации метода. Посмотрите на следующий код:

      type Creature struct {
          Species string
      }
      
      func (c Creature) String() string {
          return c.Species
      }
      

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

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

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

      Добавим метод Reset в наш тип Creature. Этот метод будет задавать для поля Species пустую строку:

      main.go

      package main
      
      import "fmt"
      
      type Creature struct {
          Species string
      }
      
      func (c Creature) Reset() {
          c.Species = ""
      }
      
      func main() {
          var creature Creature = Creature{Species: "shark"}
      
          fmt.Printf("1) %+vn", creature)
          creature.Reset()
          fmt.Printf("2) %+vn", creature)
      }
      

      Если мы запустим программу, результат будет выглядеть так:

      Output

      1) {Species:shark} 2) {Species:shark}

      Обратите внимание, что хотя в методе Reset мы задали для Species пустую строку, при выводе значения переменной creature в функции main это поле по-прежнему имеет значение shark. Это связано с тем, что мы определили метод Reset как имеющий приемник значения. Это означает, что у метода будет доступ только к копии переменной creature.

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

      main.go

      package main
      
      import "fmt"
      
      type Creature struct {
          Species string
      }
      
      func (c *Creature) Reset() {
          c.Species = ""
      }
      
      func main() {
          var creature Creature = Creature{Species: "shark"}
      
          fmt.Printf("1) %+vn", creature)
          creature.Reset()
          fmt.Printf("2) %+vn", creature)
      }
      

      Обратите внимание, что мы добавили звездочку (*) перед названием типа Creature при определении метода Reset. Это означает, что экземпляр Creature, который передается в метод Reset, теперь является указателем, и когда мы изменим его, это повлияет на оригинальный экземпляр соответствующей переменной.

      Output

      1) {Species:shark} 2) {Species:}

      Теперь Reset изменил значение поля Species.

      Заключение

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



      Source link

      Знакомство с GOPATH


      Введение

      Из этой статьи вы узнаете о GOPATH, принципах работы и настройке GOPATH. Это важный шаг для настройки среды разработки Go и понимания процесса поиска, установки и сборки исходных файлов Go. В этой статье мы будем использовать термин GOPATH для описания обсуждаемой концепции структуры папок. Термин $GOPATH будет означать переменную среды, используемую Go для поиска структуры папок.

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

      Настройка переменной среды $GOPATH

      Переменная среды $GOPATH перечисляет места, где Go ищет рабочие пространства Go.

      По умолчанию Go использует в качестве GOPATH расположение $HOME/go, где $HOME — корневой каталог учетной записи пользователя нашего компьютера. Для изменения этой настройки следует изменить переменную среды $GOPATH. Дополнительную информацию можно найти в обучающем модуле по чтению и настройке переменных среды в Linux.

      Дополнительную информацию по настройке переменной $GOPATH можно найти в документации по Go.

      Кроме того, из этой серии вы можете подробно узнать об установке Go и настройке среды разработки Go.

      $GOPATH — это не $GOROOT

      Переменная $GOROOT определяет расположение кода Go, компилятора и инструментов, а не нашего исходного кода. Переменная $GOROOT обычно имеет значение вида /usr/local/go. Переменная $GOPATH обычно имеет значение вида $HOME/go.

      Хотя явно задавать переменную $GOROOT больше не нужно, она все еще упоминается в старых материалах.

      Теперь поговорим о структуре рабочего пространства Go.

      Анатомия рабочего пространства Go

      Внутри рабочего пространства Go или GOPATH содержится три каталога: bin, pkg и src. Каждый из этих каталогов имеет особое значение для цепочки инструментов Go.

      .
      ├── bin
      ├── pkg
      └── src
        └── github.com/foo/bar
          └── bar.go
      

      Давайте посмотрим на каждый из этих каталогов.

      Каталог $GOPATH/bin — это место, где Go размещает двоичные файлы, компилируемые go install. Операционная система использует переменную среды $PATH для поиска двоичных приложений, которые могут выполняться без полного пути. Рекомендуется добавить этот каталог в глобальную переменную $PATH.

      Например, если мы не добавим $GOPATH/bin в $PATH для выполнения программы, нам нужно будет выполнять запуск следующим образом:

      При добавлении $GOPATH/bin в $PATH мы можем вызвать программу примерно так:

      Каталог $GOPATH/pkg используется Go для хранения предварительно скомпилированных объектных файлов для ускорения последующей компиляции программ. Большинству разработчиков этот каталог не потребуется. Если у вас возникнут проблемы при компиляции, вы можете спокойно удалить этот каталог, и Go воссоздаст его.

      В каталоге src должны находиться все наши файлы .go или исходный код. Их не следует путать с исходным кодом, который используют инструменты Go, и который находится в каталоге $GOROOT. При написании приложений, пакетов и библиотек Go мы помещаем эти файлы в каталог $GOPATH/src/path/to/code.

      Что такое пакеты?

      Код Go организован в виде пакетов. Пакет представляет собой все файлы в одном каталоге на диске. Один каталог может содержать только определенные файлы из этого же пакета. Пакеты хранятся вместе со всеми созданными пользователем исходными файлами Go в каталоге $GOPATH/src. Импорт разных пакетов поможет легко понять принципы обработки пакетов.

      Если наш код находится в каталоге $GOPATH/src/blue/red, его имя пакета будет red.

      Выражение импорта пакета red будет выглядеть следующим образом:

      import "blue/red"
      

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

      Например, мы можем импортировать исходный код по адресу https://github.com/gobuffalo/buffalo, используя следующий путь импорта:

      import "github.com/gobuffalo/buffalo"
      

      Поэтому исходный код будет располагаться в следующем каталоге на диске:

      $GOPATH/src/github.com/gobuffalo/buffalo
      

      Заключение

      В этой статье мы рассказали о GOPATH как о наборе папок, где Go ожидает видеть наш исходный код, описали эти папки и рассказали об их содержимом. Также мы показали, как можно сменить расположение по умолчанию $HOME/go на выбранное пользователем расположение посредством настройки переменной среды $GOPATH. В заключение мы показали, как Go ищет пакеты в структуре папок.

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

      GOPATH — один из сложных аспектов настройки Go, однако после завершения настройки о нем можно забыть.



      Source link