One place for hosting & domains

      Определение

      Определение методов в Go


      Введение

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

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

      Синтаксис для определения метода аналогичен синтаксису для определения функции. Единственная разница — это добавление дополнительного параметра после ключевого слова func для указания получателя метода. Получатель — это объявление типа, для которого вы хотите определить метод. В следующем примере определяется метод для типа структуры:

      package main
      
      import "fmt"
      
      type Creature struct {
          Name     string
          Greeting string
      }
      
      func (c Creature) Greet() {
          fmt.Printf("%s says %s", c.Name, c.Greeting)
      }
      
      func main() {
          sammy := Creature{
              Name:     "Sammy",
              Greeting: "Hello!",
          }
          Creature.Greet(sammy)
      }
      

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

      Output

      Sammy says Hello!

      Мы создали структуру с именем Creature с полями типа string для Name и Greeting. Эта структура Creature имеет один определенный метод Greet. В объявлении получателя мы присвоили экземпляр Creature для переменной с, чтобы мы могли обращаться к полям Creature, когда мы будем собирать сообщение приветствия в fmt.Printf.

      В других языках вызовы получателя метода обычно выполняются с помощью ключевого слова (например, this или self). Go рассматривает получателя как обычную переменную, поэтому вы можете использовать любое имя на ваше усмотрение. Сообществом для данного параметра используется стиль, согласно которому имя типа получателя должно начинаться со строчной буквы. В данном примере мы использовали c, поскольку типом получателя является Creature.

      Внутри тела main мы создали экземпляр Creature и указали значения для полей Name и Greeting. Здесь мы вызвали метод Greet, объединив имя типа и имя метода с помощью оператора . и предоставив экземпляр Creature в качестве первого аргумента.

      Go предоставляет другой, более удобный способ вызова методов для экземпляров структуры, как показано в данном примере:

      package main
      
      import "fmt"
      
      type Creature struct {
          Name     string
          Greeting string
      }
      
      func (c Creature) Greet() {
          fmt.Printf("%s says %s", c.Name, c.Greeting)
      }
      
      func main() {
          sammy := Creature{
              Name:     "Sammy",
              Greeting: "Hello!",
          }
          sammy.Greet()
      }
      

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

      Output

      Sammy says Hello!

      Этот пример идентичен предыдущему, но в этот раз мы использовали запись через точку для вызова метода Greet с помощью Creature, который хранится в переменной sammy как получатель. Это сокращенная форма записи для вызова функции в первом примере. Стандартная библиотека и сообщество Go предпочитают использовать этот стиль, так что вы редко будете видеть стиль вызова, показанный ранее.

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

      package main
      
      import "fmt"
      
      type Creature struct {
          Name     string
          Greeting string
      }
      
      func (c Creature) Greet() Creature {
          fmt.Printf("%s says %s!n", c.Name, c.Greeting)
          return c
      }
      
      func (c Creature) SayGoodbye(name string) {
          fmt.Println("Farewell", name, "!")
      }
      
      func main() {
          sammy := Creature{
              Name:     "Sammy",
              Greeting: "Hello!",
          }
          sammy.Greet().SayGoodbye("gophers")
      
          Creature.SayGoodbye(Creature.Greet(sammy), "gophers")
      }
      

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

      Output

      Sammy says Hello!! Farewell gophers ! Sammy says Hello!! Farewell gophers !

      Мы изменили предыдущие примеры для введения другого метода под названием SayGoodbye, а также изменили Greet, который будет возвращать Creature, чтобы мы могли использовать дополнительные методы для данного экземпляра. В теле main мы вызываем методы Greet и SayGoodbye для переменной sammy, вначале используя запись с точкой, а потом используя стиль функционального вызова.

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

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

      Интерфейсы

      Когда вы определили метод для любого типа в Go, этот метод добавляется в набор методов для типа. Набор методов — это коллекция функций, связанных с этим типом как методы и используемых компилятором Go для определения того, может ли определенный тип быть связан с переменной с типом интерфейса. Тип интерфейса — это спецификация методов, используемых компилятором для гарантии того, что тип обеспечивает реализацию этих методов. Любой тип, который имеет методы с тем же именем, теми же параметрами и теми же возвращаемыми значениями, что и методы, которые находятся в определении интерфейса, реализует этот интерфейс и может привязываться к переменным с данным типом интерфейса. Ниже приводится определение интерфейса fmt.Stringer из стандартной библиотеки:

      type Stringer interface {
        String() string
      }
      

      Чтобы тип реализовывал интерфейс fmt.Stringer, он должен иметь метод String(), который возвращает строку. Реализация этого интерфейса позволит вашему типу выводить данные так, как вы хотите (иногда это называется “наглядным выводом”), когда вы передаете экземпляры своего типа функциям, определенным в пакете fmt. Следующий пример определяет тип, который реализует этот интерфейс:

      package main
      
      import (
          "fmt"
          "strings"
      )
      
      type Ocean struct {
          Creatures []string
      }
      
      func (o Ocean) String() string {
          return strings.Join(o.Creatures, ", ")
      }
      
      func log(header string, s fmt.Stringer) {
          fmt.Println(header, ":", s)
      }
      
      func main() {
          o := Ocean{
              Creatures: []string{
                  "sea urchin",
                  "lobster",
                  "shark",
              },
          }
          log("ocean contains", o)
      }
      

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

      Output

      ocean contains : sea urchin, lobster, shark

      В данном примере определяется новый тип структуры под названием Ocean. Ocean, как сказано, реализует интерфейс fmt.Stringer, поскольку Ocean определяет метод под названием String, который не принимает никаких параметров и возвращает строку. В main мы определили новый экземпляр Ocean и передали его функции log, которая получает строку для вывода, после чего следует что-то, реализующее fmt.Stringer. Компилятор Go позволяет нам передавать o здесь, поскольку Ocean реализует все методы, запрашиваемые fmt.Stringer. Внутри log мы используем fmt.Println и вызываем метод String из Ocean, когда он получает fmt.Stringer в качестве одного из своих параметров.

      Если Ocean не предоставляет метод String(), Go будет генерировать ошибку компиляции, поскольку метод log запрашивает fmt.Stringer в качестве аргумента. Эта ошибка выглядит следующим образом:

      Output

      src/e4/main.go:24:6: cannot use o (type Ocean) as type fmt.Stringer in argument to log: Ocean does not implement fmt.Stringer (missing String method)

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

      Output

      src/e4/main.go:26:6: cannot use o (type Ocean) as type fmt.Stringer in argument to log: Ocean does not implement fmt.Stringer (wrong type for String method) have String() want String() string

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

      Получатели по указателю

      Синтаксис для определения методов для получателя по указателю практически полностью идентичен определению методов для получателя по значению. Разница состоит в добавлении префикса к имени типа в объявлении получателя со звездочкой (*). Следующий пример определяет метод для получателя по указателю для типа:

      package main
      
      import "fmt"
      
      type Boat struct {
          Name string
      
          occupants []string
      }
      
      func (b *Boat) AddOccupant(name string) *Boat {
          b.occupants = append(b.occupants, name)
          return b
      }
      
      func (b Boat) Manifest() {
          fmt.Println("The", b.Name, "has the following occupants:")
          for _, n := range b.occupants {
              fmt.Println("t", n)
          }
      }
      
      func main() {
          b := &Boat{
              Name: "S.S. DigitalOcean",
          }
      
          b.AddOccupant("Sammy the Shark")
          b.AddOccupant("Larry the Lobster")
      
          b.Manifest()
      }
      

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

      Output

      The S.S. DigitalOcean has the following occupants: Sammy the Shark Larry the Lobster

      Данный пример определяет тип Boat с полями Name и occupants. Мы хотим использовать код в других пакетах, чтобы добавить пассажиров с помощью метода AddOccupant, поэтому мы сделали поле occupants неэкспортируемым, указав первую букву имени поля строчной. Также мы должны убедиться, что вызов AddOccupant приводит к изменению экземпляра Boat, и поэтому мы определили AddOccupant для получателя по указателю. Указатель выступает в качестве ссылки на конкретный экземпляр типа, а не на копию этого типа. Зная, что AddOccupant будет вызываться с помощью указателя на Boat, вы гарантируете, что любые изменения будут сохранены.

      Внутри main мы определяем новую переменную b, которая будет хранить указатель на Boat (*Boat). Мы вызываем метод AddOccupant дважды для данного экземпляра, чтобы добавить двух пассажиров. Метод Manifest определяется для значения Boat, поскольку в определении получатель указан как (b Boat). В main мы все еще можем вызвать Manifest, поскольку Go может автоматически разыменовывать указатель для получения значения Boat. Здесь b.Manifest() является эквивалентом (*b). Manifest().

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

      Получатели по указателю и интерфейсы

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

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

      package main
      
      import "fmt"
      
      type Submersible interface {
          Dive()
      }
      
      type Shark struct {
          Name string
      
          isUnderwater bool
      }
      
      func (s Shark) String() string {
          if s.isUnderwater {
              return fmt.Sprintf("%s is underwater", s.Name)
          }
          return fmt.Sprintf("%s is on the surface", s.Name)
      }
      
      func (s *Shark) Dive() {
          s.isUnderwater = true
      }
      
      func submerge(s Submersible) {
          s.Dive()
      }
      
      func main() {
          s := &Shark{
              Name: "Sammy",
          }
      
          fmt.Println(s)
      
          submerge(s)
      
          fmt.Println(s)
      }
      

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

      Output

      Sammy is on the surface Sammy is underwater

      Данный пример определяет интерфейс под названием Submersible, который требует типы с методом Dive(). Затем мы определили тип Shark с полем Name и методом isUnderwater для отслеживания состояния Shark. Мы определили метод Dive() для получателя по указателю для типа Shark, который изменяет возвращаемое методом isUnderwater значение на true. Также мы определили метод String() получателя по значению, чтобы он мог полностью выводить на экран состояние Shark, используя fmt.Println путем применения интерфейса fmt.Stringer, принимаемого fmt.Println, который мы рассматривали ранее. Также мы использовали функцию submerge, которая получает параметр Submersible.

      Использование интерфейса Submersible вместо *Shark позволяет функции submerge опираться исключительно на поведение, предоставляемое по типу. Это делает функцию submerge более подходящей для повторного использования, поскольку вы не должны будете писать новые функции submerge для Submarine, Whale или любого будущего обитателя моря, которого у нас еще нет. Если они определяют метод Dive(), они могут использоваться с функцией submerge.

      Внутри main мы определили переменную s, которая указывает на Shark, и немедленно вывели s с помощью fmt.Println. В результате вы увидите первую часть вывода, Sammy is on the surface. Мы передали s в функцию submerge, а потом вызвали fmt.Println еще раз с s в качестве аргумента, чтобы увидеть вторую часть вывода на экране, Sammy is underwater.

      Если бы мы изменили s на Shark, а не на *Shark, компилятор Go выдал бы ошибку:

      Output

      cannot use s (type Shark) as type Submersible in argument to submerge: Shark does not implement Submersible (Dive method has pointer receiver)

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

      Заключение

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

      Если вы хотите узнать больше о языке программирования Go в целом, ознакомьтесь с нашей серией статей о программировании на языке Go.



      Source link

      Определение структур в Go


      Введение

      Создание абстракций вокруг конкретных деталей — это самый лучший инструмент, который язык программирования может предоставить разработчику. Структуры позволяют разработчикам Go описывать мир, в котором работает программа Go. Вместо того, чтобы беспокоиться об использовании строк, описывающих Street, City или PostalCode, структуры позволяют нам говорить об Address. Они служат естественным связующим звеном для документирования в рамках наших усилий по информированию будущих разработчиков (включая нас самих) о том, какие данные важны для наших программ Go и как будущий код должен использовать эти данные надлежащим образом. Структуры могут быть определены и использованы несколькими способами. В этом обучающем руководстве мы рассмотрим каждый из этих способов.

      Определение структур

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

      Чтобы создать новую структуру, вы должны вначале предоставить Go проект, описывающий поля, которые содержит структура. Это определение структуры обычно начинается с ключевого слова type, после которого идет имя структуры. После этого нужно использовать ключевое слово struct, за которым следует пара скобок {}, где вы объявляете поля, которые будет содержать структура. После того как вы определили структуру, вы можете объявить переменные, которые используют это определение структуры. В данном примере определяется и используется структура:

      package main
      
      import "fmt"
      
      type Creature struct {
          Name string
      }
      
      func main() {
          c := Creature{
              Name: "Sammy the Shark",
          }
          fmt.Println(c.Name)
      }
      

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

      output

      Sammy the Shark

      Сначала мы определяем структуру Creature в данном примере, которая содержит поле Name типа string. Внутри тела main мы создадим экземпляр Creature, поместив пару скобок после имени типа, Creature, а затем указав значения для полей этого экземпляра. Экземпляр в c будет иметь в поле Name значение “Sammy the Shark”. Внутри вызова функции fmt.Println мы будем получать значения поля экземпляра, указав точку после переменной, где был создан экземпляр, за которой следует имя поля, которое мы хотели бы получить. Например, c.Name в данном случае возвращает значение поля Name.

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

      package main
      
      import "fmt"
      
      type Creature struct {
          Name string
          Type string
      }
      
      func main() {
          c := Creature{"Sammy", "Shark"}
          fmt.Println(c.Name, "the", c.Type)
      }
      

      Вывод будет таким же, как в последнем примере:

      output

      Sammy the Shark

      Мы добавили дополнительное поле в Creature для отслеживания типа создания в виде строки. При создании экземпляра Creature внутри тела main мы предпочли использовать более короткую форму создания экземпляра, указав значения для каждого поля по порядку и опустив названия полей. В объявлении Creature{"Sammy", "Shark"} поле Name получает значение Sammy, а поле Type получает значение Shark, поскольку Name появляется первым в объявлении, а уже за ним идет Type.

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

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

      Экспорт поля структуры

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

      package main
      
      import "fmt"
      
      type Creature struct {
          Name string
          Type string
      
          password string
      }
      
      func main() {
          c := Creature{
              Name: "Sammy",
              Type: "Shark",
      
              password: "secret",
          }
          fmt.Println(c.Name, "the", c.Type)
          fmt.Println("Password is", c.password)
      }
      

      Результат будет выглядеть так:

      output

      Sammy the Shark Password is secret

      Мы добавили дополнительное поле в наши предыдущие примеры, secret. secret — это неэкспортируемое поле типа string, что значит, что любой другой пакет, который пытается создать экземпляр Creature, не сможет получить доступ или задать значение для поля secret. Внутри этого пакета мы можем получить доступ к этим полям, как это сделано в данном примере. Поскольку main также находится в пакете main, эта функция может запросить c.password и получить сохраненное там значение. Неэкспортируемые поля в структурах встречаются часто, а доступ к таким полям осуществляется через экспортируемые методы.

      Вложенные структуры

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

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

      package main
      
      import "fmt"
      
      func main() {
          c := struct {
              Name string
              Type string
          }{
              Name: "Sammy",
              Type: "Shark",
          }
          fmt.Println(c.Name, "the", c.Type)
      }
      

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

      output

      Sammy the Shark

      Вместо того, чтобы определять новый тип, описывающий структуру с ключевым словом type, в данном примере вложенная структура определяется сразу же после короткого оператора присваивания :=. Мы определяем поля структуры, как и в предыдущих примерах, но теперь мы должны немедленно предоставить другую пару скобок и значения, которые будут присваиваться каждому полю. Использование структуры остается прежним, мы можем обратиться к именам полей с помощью записи с точкой. Чаще всего вы будете видеть вложенные структуры в тестах, так как часто используемые один раз структуры определяются для хранения данных и ожиданий для конкретного тестового случая.“”“

      Заключение

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



      Source link

      Определение и вызов функций в Go


      Введение

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

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

      • fmt.Println(), которая будет выводить объекты в стандартный вывод (скорее всего, это будет ваш терминал).
      • fmt.Printf(), которая позволяет форматировать отображаемый результат.

      Имена функций включают скобки и могут содержать параметры.

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

      Определение функции

      Давайте начнем с превращения классического примера “Hello, World!” из программы в функцию.

      Мы создадим новый текстовый файл в текстовом редакторе по выбору и вызовем программу hello.go. Затем мы определим функцию.

      Функция определяется с помощью ключевого слова func. За ним следует имя функции и набор скобок, которые хранят любые параметры, принимаемые функцией (скобки могут быть пустыми). Строки кода функции помещаются в фигурные скобки {}.

      В данном случае мы определим функцию с именем hello():

      hello.go

      func hello() {}
      

      Это первоначальное объявление для создания функции.

      Теперь мы можем добавить вторую строку для определения того, что будет делать наша функция. В данном случае мы будем выполнять вывод Hello, World! в консоль:

      hello.go

      func hello() {
          fmt.Println("Hello, World!")
      }
      

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

      Давайте внутри нашего блока функции main() вызовем функцию с именем hello():

      hello.go

      package main
      
      import "fmt"
      
      func main() {
          hello()
      }
      
      func hello() {
          fmt.Println("Hello, World!")
      }
      

      Теперь мы запустим программу:

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

      Output

      Hello, World!

      Обратите внимание, что мы также ввели функцию main(). Функция main() — это особая функция, которая указывает компилятору, что это место, откуда должно начинаться выполнение программы. Для любой программы, которая будет исполняемой (программа, которая может запускаться из командной строки), вам потребуется функция main(). Функция main() должна использоваться только один раз и находиться в пакете main(), она не имеет аргументов и возвращаемого значения. Это позволяет реализовать исполнение в любой программе Go. Как показано в следующем примере:

      main.go

      package main
      
      import "fmt"
      
      func main() {
          fmt.Println("this is the main section of the program")
      }
      

      Функции могут быть более сложными, чем функция hello(), которую мы определили. Мы можем использовать циклы for, условные операторы и многое другое внутри нашей функции.

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

      names.go

      package main
      
      import (
          "fmt"
          "strings"
      )
      
      func main() {
          names()
      }
      
      func names() {
          fmt.Println("Enter your name:")
      
          var name string
          fmt.Scanln(&name)
          // Check whether name has a vowel
          for _, v := range strings.ToLower(name) {
              if v == 'a' || v == 'e' || v == 'i' || v == 'o' || v == 'u' {
                  fmt.Println("Your name contains a vowel.")
                  return
              }
          }
          fmt.Println("Your name does not contain a vowel.")
      }
      

      Функция names(), которую мы определили здесь, устанавливает значение переменной name, а затем задает условный оператор внутри цикла for. Здесь показано, как можно организовывать код внутри функции. Однако в зависимости от того, что мы намерены делать в нашей программе и как мы хотим организовать наш код, мы можем использовать условный оператор и цикл for в качестве двух отдельных функций.

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

      Работа с параметрами

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

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

      Давайте создадим программу, которая повторяет слово определенное количество раз. Она будет получать параметр string с именем word и параметр int с именем reps для количества повторений этого слова.

      repeat.go

      package main
      
      import "fmt"
      
      func main() {
          repeat("Sammy", 5)
      }
      
      func repeat(word string, reps int) {
          for i := 0; i < reps; i++ {
              fmt.Print(word)
          }
      }
      

      Мы передали значение Sammy для параметра word и 5 для параметра reps. Эти значения соответствуют каждому параметру в заданном нами порядке. Функция repeat имеет цикл for, который будет выполняться количество раз, определенное значением параметра reps. Для каждой итерации значение параметра word выводится на экран.

      Здесь вы увидите вывод программы:

      Output

      SammySammySammySammySammy

      Если у вас есть набор параметров, которые имеют одинаковое значение, вы можете каждый раз не указывать тип значения. Давайте создадим небольшую программу, которая получает параметры x, y и z, имеющие тип int. Мы создадим функцию, которая складывает значения параметров в разных конфигурациях. Получаемые суммы будут выводиться функцией. Затем мы будем вызывать функцию и передавать числа в эту функцию.

      add_numbers.go

      package main
      
      import "fmt"
      
      func main() {
          addNumbers(1, 2, 3)
      }
      
      func addNumbers(x, y, z int) {
          a := x + y
          b := x + z
          c := y + z
          fmt.Println(a, b, c)
      }
      

      Когда мы создали сигнатуру функции для addNumbers, не нужно было указывать тип каждый раз, а только в конце.

      Мы передали число 1 в параметр x, 2 в параметр y и 3 в параметр z. Эти значения соответствуют каждому параметру в заданном нами порядке.

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

      a = 1 + 2
      b = 1 + 3
      c = 2 + 3
      

      Функция также выводит a, b и c, и на основе этих математических операций мы ожидаем, что a будет равна 3, b4 и c5. Давайте запустим программу:

      Output

      3 4 5

      Когда мы передадим 1, 2 и 3 в качестве параметров функции addNumbers(), мы получаем ожидаемый результат.

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

      Возврат значения

      Вы можете передать значение параметра в функцию, а функция может также генерировать значение.

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

      До настоящего момента мы использовали fmt.Println() вместо оператора return в наших функциях. Давайте создадим программу, которая вместо вывода на экран будет возвращать переменную.

      В новом текстовом файле с именем double.go мы создадим программу, которая удваивает параметр x и возвращает переменную y. Мы осуществляем вызов для вывода переменной result, которая создается при запуске функции double() с переданным ей значением 3:

      double.go

      package main
      
      import "fmt"
      
      func main() {
          result := double(3)
          fmt.Println(result)
      }
      
      func double(x int) int {
          y := x * 2
          return y
      }
      
      

      Мы можем запустить программу и увидеть следующий вывод:

      Output

      6

      Целое число 6 возвращается как результат, что является ожидаемым значением при умножении 3 на 2.

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

      Мы можем продемонстрировать это, закомментировав строку с оператором return:

      double.go

      package main
      
      import "fmt"
      
      func main() {
          result := double(3)
          fmt.Println(result)
      }
      
      func double(x int) int {
          y := x * 2
          // return y
      }
      
      

      Теперь мы снова запустим программу:

      Output

      ./double.go:13:1: missing return at end of function

      Без оператора return программу не удастся скомпилировать.

      Выполнение функции прекращается немедленно при достижении оператора return, даже если он находится не в конце функции:

      return_loop.go

      package main
      
      import "fmt"
      
      func main() {
          loopFive()
      }
      
      func loopFive() {
          for i := 0; i < 25; i++ {
              fmt.Print(i)
              if i == 5 {
                  // Stop function at i == 5
                  return
              }
          }
          fmt.Println("This line will not execute.")
      }
      

      Здесь мы используем цикл for и выполняем 25 итераций данного цикла. Однако внутри цикла for у нас есть условный оператор if, который проверяет, имеет ли i значение 5. Если условие выполняется, выполняется оператор return. Поскольку мы находимся внутри функции loopFive, срабатывание оператора return внутри этой функции приводит к прекращению ее работы. В результате мы никогда не доберемся до последней строки, которая выводит строку This line will not execute.​​

      Использование оператора return внутри цикла for позволяет завершить работу функции, так что строка, находящаяся вне цикла, не будет выполняться. Если же, напротив, мы бы использовали оператор break, прекращалась только работа цикла, а последняя строка fmt.Println() все равно бы выполнялась.

      Оператор return прекращает работу функции и может возвращать значение, если оно указано в сигнатуре функции.

      Возврат нескольких значений

      Для функции может быть указано более одного возвращаемого значения. Давайте посмотрим на программу repeat.go и сделаем так, чтобы она возвращала два значения. Первым значением будет повторяющееся значение, а второе будет ошибкой, которая возникает, если параметр reps имеет значение меньше 0:

      repeat.go

      package main
      
      import "fmt"
      
      func main() {
          val, err := repeat("Sammy", -1)
          if err != nil {
              fmt.Println(err)
              return
          }
          fmt.Println(val)
      }
      
      func repeat(word string, reps int) (string, error) {
          if reps <= 0 {
              return "", fmt.Errorf("invalid value of %d provided for reps. value must be greater than 0.", reps)
          }
          var value string
          for i := 0; i < reps; i++ {
              value = value + word
          }
          return value, nil
      }
      

      Первое, что делает функция repeat, — это проверка действительности значения аргумента reps. Любое значение меньше 0 будет вызывать ошибку. После того как мы передали значение -1, эта часть кода будет выполнена. Обратите внимание, что при выполнении возврата из функции, мы должны предоставить возвращаемые значения типа string и error. Поскольку предоставленные аргументы привели к ошибке, мы передадим пустую строку для первого возвращаемого значения и ошибку для второго возвращаемого значения.

      В функции main() мы можем получить оба возвращаемых значения, объявив две новые переменные, value и err. Поскольку в возвращаемом значении может быть ошибка, нам нужно проверить, получаем ли мы ошибку, прежде чем продолжить работу с нашей программой. В данном примере мы получили ошибку. Мы выводим ошибку и с помощью return выходим из функции main() для выхода из программы.

      Если ошибки не было, мы выводим возвращаемое значение функции.

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

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

      Output

      invalid value of -1 provided for reps. value must be greater than 0.

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

      Заключение

      Функции — это блоки кода с инструкциями, которые выполняются внутри программы и помогают сделать код модульным и доступным для многоразового использования.

      Чтобы узнать больше о том, как сделать ваш код более модульным, вы можете ознакомиться с нашим руководством Написание пакетов в Go.



      Source link