Пакети та видимість¶
Програму на Go організовано в пакети. Пакет — це каталог із файлами
.go, які компілюються разом і ділять один простір імен. Кожен вихідний
файл починається з оголошення package, що називає пакет, до якого він
належить.
Усе інше тримається на двох правилах:
- Один каталог = один пакет. Усі файли
.goв каталозі мають оголошувати однакову назву пакета; разом вони утворюють цей пакет. (Є рівно один виняток — зовнішній тестовий пакет, див. нижче.) package mainособливий — це точка входу виконуваної програми, і він мусить міститиfunc main(). Кожен інший пакет — це бібліотека, яку імпортують інші.
Шляхи імпорту проти назв пакетів¶
Пакет ви імпортуєте за його шляхом імпорту (його розташуванням), а
звертаєтеся до нього за назвою пакета (ідентифікатором в оголошенні
package). Зазвичай це останній елемент шляху, але не завжди:
Назва пакета — це те, що з'являється в коді, тож її обирають за тим, як вона читається в місці виклику.
Експортоване проти неекспортованого: керування доступом — це регістр¶
Ідентифікатор, чия назва починається з великої літери, є експортованим — видимим коду в інших пакетах. З малої — неекспортований — видимий лише всередині свого пакета. Це стосується кожного імені верхнього рівня: типів, функцій, змінних, констант і полів структур.
package store
type Item struct {
Name string // експортоване поле
price int // неекспортоване поле
}
func New() *Item { return &Item{} } // експортована
func reset(i *Item) { i.price = 0 } // неекспортована
Межа приватності — це пакет, а не тип чи файл. Файли в одному пакеті вільно бачать неекспортовані імена одне одного; інший пакет бачить лише експортовані. З іншого пакета компілятор це забезпечує:
// у пакеті main, що імпортує пакет store вище
i := store.New()
fmt.Println(i.Name) // ок — Name експортоване
fmt.Println(i.price) // compile error: i.price undefined
// (cannot refer to unexported field price)
З погляду Python: немає ні суто рекомендаційної домовленості
_private, ні спотворення імен__name— видимість забезпечує компілятор, а одиниця приватності — це пакет, а не клас.
Файли в одному пакеті¶
Розбиття пакета на кілька файлів суто організаційне — файли ділять один простір імен, і порядок оголошень не має значення (функція може викликати іншу, оголошену пізніше, у будь-якому файлі пакета).
// item.go
package store
func New() *Item { return &Item{price: basePrice} }
// price.go
package store
const basePrice = 100 // видима для item.go без жодного імпорту
Єдиний виняток: foo та foo_test¶
Каталог може містити другий пакет, і лише один: <name>_test. Тестові
файли, що оголошують package store_test, живуть поруч із package store
і компілюються окремо — вони можуть користуватися лише експортованим
API, точно як будь-який інший викликач.
// store/store.go
package store
func New() *Item { return &Item{price: basePrice} }
// store/internal_test.go — той самий пакет: бачить неекспортовані імена
package store
func TestBasePrice(t *testing.T) { _ = basePrice }
// store/store_test.go — зовнішній: лише експортований API
package store_test
import "example.com/shop/store"
func TestNew(t *testing.T) { _ = store.New() }
go list показує три групи, які відстежують інструменти. Прапорець
-f приймає шаблон у тому самому синтаксисі {{ }}, що й пакет
text/template Go: {{.GoFiles}} друкує поле GoFiles кожного пакета.
$ go list -f '{{.GoFiles}} {{.TestGoFiles}} {{.XTestGoFiles}}' ./store
[store.go] [internal_test.go] [store_test.go]
Писати тести в package foo_test — це спосіб самому скуштувати власний
публічний API: якщо тест писати незручно, то й API незручний. Два
звичайні пакети в одному каталозі лишаються помилкою:
Функції init¶
Пакет може оголосити одну або кілька func init() — без параметрів, без
результатів. Вони запускаються автоматично під час ініціалізації
пакета, після того як налаштовано всі змінні рівня пакета, і перед стартом
main. Імпорти пакета ініціалізуються першими, тож на момент запуску
вашого init усе, від чого ви залежите, вже готове.
package config
var settings map[string]string
func init() {
settings = map[string]string{"env": "dev"}
}
Порядок — спершу змінні рівня пакета, потім init, потім main — можна
спостерігати:
var x = setup()
func setup() int { fmt.Println("var init"); return 1 }
func init() { fmt.Println("init func") }
func main() { fmt.Println("main") }
// output:
// var init
// init func
// main
Користуйтеся init помірно — для налаштування, яке справді неможливо
виразити звичайним ініціалізатором змінної. Кілька init (навіть у різних
файлах) виконуються в порядку, у якому файли подано компілятору.
Домовленості про назви¶
- Назви пакетів короткі, з малих літер, одне слово —
http,bytes,strconv. Без під_креслень чи camelCase. - Уникайте затинання. Кваліфікатор пакета вже є в місці виклику, тож
називайте
bytes.Buffer, а неbytes.BytesBuffer;store.New, а неstore.NewItem, коли пакет і так усе пояснює. - Док-коментар до пакета —
// Package store ...— розміщують над оголошеннямpackageв одному файлі, і він документує весь пакет.
Жодних циклів імпорту¶
Імпорти пакетів мають утворювати ациклічний граф: якщо a імпортує b,
то b не може імпортувати a, ні прямо, ні транзитивно. Компілятор
відхиляє цикли беззастережно. Коли два пакети ніби потребують одне одного,
це знак, що спільний тип має переїхати до третього пакета, який вони обидва
імпортують.
Швидка довідка¶
| Поняття | Правило |
|---|---|
| оголошення package | кожен файл .go починається з package X |
| один каталог | рівно один пакет |
package main + func main() |
збирає виконувану програму |
| шлях імпорту | де пакет; вживається в import |
| назва пакета | як ви звете його в коді |
Uppercase |
експортоване (видиме іншим пакетам) |
lowercase |
неекспортоване (приватне для пакета) |
func init() |
запускається під час ініціалізації пакета, перед main |
| цикл імпорту | заборонено |