[Перевод] Как писать Go-пакеты
Пакет Go состоит из Go-файлов, расположенных в одной и той же директории, в начале которых имеется одинаковое выражение package
. Пакеты, подключаемые к программам, позволяют расширять их возможности. Некоторые пакеты входят в состав стандартной библиотеки Go. А это значит, что они, если вы пользуетесь Go, уже у вас установлены. Другие пакеты устанавливают с помощью команды go get
. Можно, кроме того, писать собственные Go-пакеты, создавая файлы в особых директориях, и придерживаясь правил оформления пакетов.
Материал, перевод которого мы сегодня публикуем, представляет собой руководство по разработке 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? Пользуетесь ли вы в них пакетами собственной разработки?