Menu

Пакеты и импорты в Golang: экспорт имён и несколько файлов

Как устроены пакеты в Go: объявление package, импорт стандартных пакетов и пакетов модулей, экспортируемые и неэкспортируемые имена, пакет из нескольких файлов, internal-пакеты, псевдонимы импорта, пустые импорты и ошибка «undefined» при go run main.go.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

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

fmt, math и strings это пакеты стандартной библиотеки. Они поставляются вместе с Go, поэтому «подключить» пакет значит написать его имя в блоке import. Ничего не скачивается. fmt.Println, math.Sqrt и strings.Repeat пишутся с заглавной буквы, потому что они экспортируются.

Объявление package

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

found packages greeting (b.go) and greet (greet.go) in /home/ana/shop/greet

(Единственное исключение это файлы тестов, которые могут использовать package name_test.)

package main особенный: он даёт исполняемый файл, и программа начинается с его func main(). Любой другой пакет это библиотека, предназначенная для импорта.

По соглашению имя пакета совпадает с последним элементом пути папки: код в shop/greet это package greet. Имена пакетов короткие, в нижнем регистре, из одного слова, без подчёркиваний и смешанного регистра: strconv, httptest, greet, а не string_utils или greetHelpers.

Импорт пакетов

Путь импорта это строка. Для стандартной библиотеки это путь пакета в дереве исходников Go; для всего остального он начинается с пути модуля:

import (
	"fmt"           // standard library
	"net/http"      // standard library, nested path
	"math/rand/v2"  // standard library, version 2 of math/rand

	"github.com/google/uuid"   // another module (needs go get)
	"example.com/shop/greet"   // a package in your own module
)

Путь говорит, где живёт пакет; имя это то, что вы пишете в коде. Для net/http имя http, для math/rand/v2 имя rand. Пишут http.Get, а не net/http.Get.

gofmt и goimports сортируют блок и по соглашению отделяют импорты стандартной библиотеки от остальных пустой строкой.

Неиспользуемые импорты это ошибка компиляции:

./main.go:5:2: "os" imported and not used

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

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

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

ИмяВидно снаружи пакета?
func Hello()да
func hello()нет
type User structда
поле User.Nameда
поле User.emailнет
const MaxSizeда
var defaultTimeoutнет

Попытка использовать неэкспортированное имя из другого пакета даёт ошибку undefined:

./main.go:11:20: undefined: greet.prefix

Правило важно даже в одном файле, потому что ваши типы исследуют другие пакеты. encoding/json видит только экспортируемые поля:

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

Пакет из нескольких файлов

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

shop/
├── go.mod        (module example.com/shop)
├── main.go
└── math.go
// main.go
package main

import "fmt"

func main() {
	fmt.Println(total(2, 3))
}
// math.go
package main

func total(a, b int) int { return a + b }

main.go вызывает total напрямую. Никакого #include и никаких импортов между файлами одного пакета.

Ошибка «undefined» при go run main.go

Это самая популярная в поиске проблема с пакетами в Go, и у неё одна причина. Если запустить только файл с main, компилируется только этот файл:

go run main.go
# command-line-arguments
./main.go:6:14: undefined: total

command-line-arguments это имя, которое Go даёт пакету, собранному из списка файлов, и math.go в этот список не входит. Запускайте весь пакет:

go run .
5

То же касается go build main.go. Используйте go run . и go build (или go build ./cmd/app), и проблема не появится. Если она остаётся и с go run ., проверьте, что в обоих файлах написано package main и что ни один не исключён ограничением сборки (//go:build) или суффиксом _test.go.

Свои пакеты

Подпапка вашего модуля это отдельный пакет. Импортируйте её по пути модуля из go.mod плюс папка:

shop/
├── go.mod          (module example.com/shop)
├── main.go
└── greet/
    └── greet.go
// greet/greet.go
package greet

const prefix = "Hi, "

// Hello returns a greeting for name.
func Hello(name string) string {
	return prefix + name
}
// main.go
package main

import (
	"fmt"

	"example.com/shop/greet"
)

func main() {
	fmt.Println(greet.Hello("Ana"))
}
Hi, Ana

Здесь людей сбивают с толку две вещи. Импорт это путь модуля плюс папка, а не относительный путь: "./greet" в режиме модулей не работает. И prefix остаётся скрытым от main, потому что он со строчной буквы; частью API пакета является только Hello.

internal-пакеты

Папка с именем internal ограничивает, кто может импортировать то, что внутри. Код в shop/internal/store может импортировать любой пакет с корнем в shop/, и никто снаружи. Другой модуль, который попробует, получит:

use of internal package example.com/shop/internal/store not allowed

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

Псевдонимы импорта

Дайте импорту другое локальное имя, написав его перед путём. Обычная причина: два пакета с одинаковым именем:

Оба пакета называются rand, поэтому хотя бы одному нужен псевдоним. Используйте псевдонимы для конфликтов и неясных имён; не переименовывайте пакеты ради краткости, ведь читатели знают strconv, а не ваш sc.

Пустые импорты и импорт через точку

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

import (
	"database/sql"

	_ "github.com/lib/pq" // registers the "postgres" driver with database/sql
)

Без _ импорт считался бы неиспользуемым и давал бы ошибку.

Импорт через точку (.). Помещает экспортированные имена пакета прямо в область видимости файла, так что вы пишете Println вместо fmt.Println. Он скрывает, откуда берутся имена, и не рекомендуется, кроме нескольких шаблонов в тестовых помощниках.

Циклические импорты

Go не разрешает двум пакетам импортировать друг друга, напрямую или через цепочку:

package example.com/shop
	imports example.com/shop/greet from main.go
	imports example.com/shop/other from c.go
	imports example.com/shop/greet from o.go: import cycle not allowed

Ошибка перечисляет цепочку. Два способа исправить покрывают почти все случаи:

  1. Вынесите код, нужный обоим пакетам, в третий, более низкоуровневый пакет, который импортируют оба.
  2. Если пакету A нужно только вызвать что-то из B, объявите в A маленький интерфейс и пусть тип из B ему удовлетворяет. Тогда A больше не импортирует B.

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

Частые ошибки

  • Запуск одного файла. go run main.go в пакете из нескольких файлов: используйте go run ..
  • Относительные импорты. import "./utils" в режиме модулей не работает. Используйте "example.com/yourmodule/utils".
  • Строчные имена там, где нужен экспорт. Функция, поле или метод, нужные другим пакетам, должны начинаться с заглавной буквы.
  • Имя пакета не совпадает с папкой. Допустимо, но запутывает: путь импорта говорит utils, а код говорит helpers.X. Держите их одинаковыми.
  • Пакеты-свалки. util, common и misc ничего не говорят о содержимом и притягивают циклические импорты. Называйте пакеты по тому, что они дают: money, auth, slug.

Часто задаваемые вопросы

Почему Go говорит, что функция из того же пакета undefined?

Скорее всего, вы запустили go run main.go. Если указать файл, компилируется только он, и функций из других файлов того же пакета нет: undefined: total. Запускайте go run ., который компилирует все файлы .go в папке. Также проверьте, что во всех файлах одинаковая строка package.

Как подключить пакет fmt в Go?

fmt входит в стандартную библиотеку и поставляется вместе с Go, так что устанавливать ничего не нужно. Добавьте import "fmt" после строки package и вызывайте его функции с префиксом fmt., например fmt.Println("hi"). То же касается strings, os, time, net/http и любого другого стандартного пакета.

Как импортировать свой пакет в Go?

Импортируйте его по пути модуля плюс папка. В модуле example.com/shop пакет из папки greet импортируется как import "example.com/shop/greet" и используется как greet.Hello(). Импортирующему коду видны только имена, начинающиеся с заглавной буквы.

Чем экспортируемые имена отличаются от неэкспортируемых в Go?

Имя, начинающееся с заглавной буквы (Hello, Total, User.Name), экспортируется, и его можно использовать из других пакетов. Имя со строчной буквы (hello, total, user.name) не экспортируется и видно только внутри своего пакета. Ключевых слов public и private нет.

Как исправить «import cycle not allowed» в Go?

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

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ