[Перевод] Как писать Go-пакеты

Пакет Go состоит из Go-файлов, расположенных в одной и той же директории, в начале которых имеется одинаковое выражение package. Пакеты, подключаемые к программам, позволяют расширять их возможности. Некоторые пакеты входят в состав стандартной библиотеки Go. А это значит, что они, если вы пользуетесь Go, уже у вас установлены. Другие пакеты устанавливают с помощью команды go get. Можно, кроме того, писать собственные Go-пакеты, создавая файлы в особых директориях, и придерживаясь правил оформления пакетов.

htdv4cfnn8nmki3yphorht5eiki.jpeg

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

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


  • Настройте программное окружение Go (о том, как это сделать, можно узнать здесь). Создайте рабочее пространство Go (этому посвящён пятый пункт вышеупомянутого материала). В следующем разделе этого материала вы сможете найти примеры, которые рекомендуется воспроизвести у себя. Так вы сможете лучше с ними разобраться.
  • Для того чтобы углубить свои знания по GOPATH — взгляните на этот материал.


Написание и импорт пакетов


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

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

└── $GOPATH
    └── src
        └── github.com
            └── gopherguides


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

└── $GOPATH
    └── src
        └── github.com
            └── gopherguides
                └── greet


Теперь мы готовы к тому, чтобы добавить в директорию greet первый файл. Обычно файл, который является входной точкой (entry point) пакета, называют так же, как названа директория пакета. В данном случае это означает, что мы, в директории greet, создаём файл greet.go:

└── $GOPATH
    └── src
        └── github.com
            └── gopherguides
                └── greet
                    └── greet.go


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

Откройте файл greet.go в текстовом редакторе и добавьте в него следующий код:

package greet

import "fmt"

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


Разберём содержимое этого файла. Первая строка каждого файла должна содержать имя пакета, в котором мы работаем. Так как мы находимся в пакете greet — здесь используется ключевое слово package, за которым следует имя пакета:

package greet


Это сообщает компилятору о том, что он должен воспринимать всё, что находится в файле, как часть пакета greet.

Далее выполняется импорт необходимых пакетов с помощью выражения import. В данном случае нам нужен всего один пакет — fmt:

import "fmt"


И, наконец, мы создаём функцию Hello. Она будет использовать возможности пакета fmt для вывода на экран строки Hello, World!:

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


Теперь, после того, как создан пакет greet, вы можете воспользоваться им в любом другом пакете. Создадим новый пакет, в котором воспользуемся пакетом greet.

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

└── $GOPATH
    └── src
        └── github.com
            └── gopherguides
                    └── example


Теперь создаём файл, являющийся входной точкой пакета. Данный пакет мы рассматриваем как выполняемую программу, а не как пакет, код которого планируется использовать в других пакетах. Файлы, являющиеся входными точками программ, принято называть main.go:

└── $GOPATH
    └── src
        └── github.com
            └── gopherguides
                └── example
                    └── main.go


Откройте в редакторе файл main.go и внесите в него следующий код, который позволяет воспользоваться возможностями пакета greet:

package main

import "github.com/gopherguides/greet"

func main() {
    greet.Hello()
}


Мы импортировали в файле main.go пакет greet, а это значит, что для вызова функции, объявленной в этом пакете, нам понадобится воспользоваться точечной нотацией. Точечная нотация — это конструкция, в которой между именем пакета и именем ресурса этого пакета, который нужно использовать, ставится точка. Например, в пакете greet роль ресурса играет функция Hello. Если нужно вызвать эту функцию — используется точечная нотация: greet.Hello().

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

go run main.go


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

Hello, World!


Теперь поговорим о том, как использовать переменные, объявляемые в пакетах. Для этого добавим объявление переменной в файл greet.go:

package greet

import "fmt"

var Shark = "Sammy"

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


Откройте файл main.go и добавьте в него строку, в которой функция fmt.Println() используется для вывода значения переменной Shark, объявленной в пакете greet.go. А именно, приведите main.go к следующему виду:

package main

import (
    "fmt"

    "github.com/gopherguides/greet"
)

func main() {
    greet.Hello()

    fmt.Println(greet.Shark)
}


Снова запустите программу:

go run main.go


Теперь она выведет следующее:

Hello, World!
Sammy


А сейчас поговорим о том, как объявлять в пакетах типы. Создадим тип Octopus с полями Name и Color, а также создадим метод типа. Этот метод, при его вызове, будет возвращать особым образом обработанное содержимое полей типа Octopus. Приведём greet.go к следующему виду:

package greet

import "fmt"

var Shark = "Sammy"

type Octopus struct {
    Name  string
    Color string
}

func (o Octopus) String() string {
    return fmt.Sprintf("The octopus's name is %q and is the color %s.", o.Name, o.Color)
}

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


Теперь откройте main.go, создайте в нём экземпляр структуры нового типа и обратитесь к его методу String():

package main

import (
    "fmt"

    "github.com/gopherguides/greet"
)

func main() {
    greet.Hello()

    fmt.Println(greet.Shark)

    oct := greet.Octopus{
        Name:  "Jesse",
        Color: "orange",
    }

    fmt.Println(oct.String())
}


После того, как вы, с помощью конструкции, которая выглядит как oct := greet.Octopus, создали экземпляр Octopus, вы можете обращаться к методам и свойствам типа из пространства имён файла main.go. Это, в частности, позволяет воспользоваться командой oct.String(), расположенной в конце файла main.go, не обращаясь к greet. Кроме того, мы можем, например, обратиться к полю структуры Color, воспользовавшись конструкцией oct.Color. При этом мы, как и тогда, когда вызывали метод, не обращаемся к greet.

Метод String типа Octopus использует функцию fmt.Sprintf для формирования предложения и возвращает, с помощью return, результат, строку, в место вызова метода (в данном случае это место находится в main.go).

Запустим программу снова:

go run main.go


Она выведет в консоль следующее:

Hello, World!
Sammy
The octopus's name is "Jesse" and is the color orange.


Теперь, когда мы оснастили Octopus методом String, мы получили механизм вывода сведений о типе, подходящий для многократного использования. Если в будущем понадобится изменить поведение этого метода, который может использоваться во многих проектах, достаточно будет один раз отредактировать его код в greet.go.

Экспорт сущностей


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

Если оснастить тип Octopus новым методом с именем reset, то этот метод можно будет вызывать из пакета greet, но не из файла main.go, который находится за пределами пакета greet. Вот обновлённый вариант greet.go:

package greet

import "fmt"

var Shark = "Sammy"

type Octopus struct {
    Name  string
    Color string
}

func (o Octopus) String() string {
    return fmt.Sprintf("The octopus's name is %q and is the color %s.", o.Name, o.Color)
}

func (o Octopus) reset() {
    o.Name = ""
    o.Color = ""
}

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


Попытаемся вызвать reset из файла main.go:

package main

import (
    "fmt"

    "github.com/gopherguides/greet"
)

func main() {
    greet.Hello()

    fmt.Println(greet.Shark)

    oct := greet.Octopus{
        Name:  "Jesse",
        Color: "orange",
    }

    fmt.Println(oct.String())

    oct.reset()
}


Это приведёт к появлению следующей ошибки компиляции:

oct.reset undefined (cannot refer to unexported field or method greet.Octopus.reset)


Для того чтобы экспортировать метод reset типа Octopus нужно его переименовать, заменив первую букву, строчную r, на прописную R. Сделаем это, отредактировав greet.go:

package greet

import "fmt"

var Shark = "Sammy"

type Octopus struct {
    Name  string
    Color string
}

func (o Octopus) String() string {
    return fmt.Sprintf("The octopus's name is %q and is the color %s.", o.Name, o.Color)
}

func (o Octopus) Reset() {
    o.Name = ""
    o.Color = ""
}

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


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

package main

import (
    "fmt"

    "github.com/gopherguides/greet"
)

func main() {
    greet.Hello()

    fmt.Println(greet.Shark)

    oct := greet.Octopus{
        Name:  "Jesse",
        Color: "orange",
    }

    fmt.Println(oct.String())

    oct.Reset()

    fmt.Println(oct.String())
}


Запустим программу:

go run main.go


Вот что попадёт в консоль:

Hello, World!
Sammy
The octopus's name is "Jesse" and is the color orange
The octopus's name is "" and is the color .


Вызвав метод Reset, мы очистили поля Name и Color нашего экземпляра Octopus. В результате, при вызове String, там, где раньше выводилось содержимое полей Name и Color, теперь не выводится ничего.

Итоги


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

Уважаемые читатели! Какие программы вы обычно пишете на Go? Пользуетесь ли вы в них пакетами собственной разработки?

itt53pns2iucwylb3bwn1fmmtnu.png

© Habrahabr.ru