Menu

Pakiety i importy w Golang: nazwy eksportowane, wiele plików

Jak działają pakiety w Go: klauzula package, importowanie pakietów standardowych i z modułów, nazwy eksportowane i nieeksportowane, podział pakietu na pliki, pakiety internal, aliasy importów, puste importy i błąd "undefined" po go run main.go.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Każdy plik Go należy do pakietu zadeklarowanego w pierwszej linii. Kod w jednym pakiecie korzysta z innego, importując go i poprzedzając nazwy nazwą pakietu. Spoza pakietu można używać tylko nazw zaczynających się wielką literą.

fmt, math i strings to pakiety biblioteki standardowej. Są dostarczane razem z Go, więc „dodanie” pakietu oznacza wpisanie jego nazwy w bloku import. Nic nie jest pobierane. fmt.Println, math.Sqrt i strings.Repeat zaczynają się wielką literą, bo są eksportowane.

Klauzula package

Pierwszą linią każdego pliku .go, która nie jest komentarzem, jest package name. Wszystkie pliki w jednym katalogu muszą używać tej samej nazwy i razem tworzą jeden pakiet. Katalog z dwiema różnymi nazwami pakietów się nie kompiluje:

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

(Jedyny wyjątek to pliki testów, które mogą używać package name_test.)

package main jest wyjątkowy: tworzy plik wykonywalny, a program startuje od jego func main(). Każdy inny pakiet jest biblioteką przeznaczoną do importowania.

Zgodnie z konwencją nazwa pakietu odpowiada ostatniemu członowi ścieżki katalogu: kod w shop/greet to package greet. Nazwy pakietów są krótkie, pisane małymi literami, jednowyrazowe, bez podkreśleń i wielkich liter w środku: strconv, httptest, greet, a nie string_utils czy greetHelpers.

Importowanie pakietów

Ścieżka importu to string. Dla biblioteki standardowej jest to ścieżka pakietu w drzewie źródeł Go; dla wszystkiego innego zaczyna się od ścieżki modułu:

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
)

Ścieżka mówi, gdzie leży pakiet; nazwa to to, co wpisujesz w kodzie. Dla net/http nazwą jest http, dla math/rand/v2 to rand. Piszesz http.Get, nigdy net/http.Get.

gofmt i goimports sortują blok i zwyczajowo oddzielają importy z biblioteki standardowej od reszty pustą linią.

Nieużywane importy to błąd kompilacji:

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

Edytory z obsługą Go uruchamiają goimports przy zapisie, co za ciebie dodaje brakujące importy standardowe i usuwa nieużywane.

Nazwy eksportowane i nieeksportowane

O widoczności w Go decyduje jedna reguła: jeśli nazwa zaczyna się wielką literą, jest eksportowana. Dotyczy to funkcji, typów, zmiennych, stałych, pól struktur i metod. Nie ma słów kluczowych public, private ani protected.

NazwaWidoczna poza pakietem?
func Hello()tak
func hello()nie
type User structtak
pole User.Nametak
pole User.emailnie
const MaxSizetak
var defaultTimeoutnie

Próba użycia nieeksportowanej nazwy z innego pakietu daje błąd undefined:

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

Reguła ma znaczenie nawet w jednym pliku, bo inne pakiety badają twoje typy. encoding/json widzi tylko pola eksportowane:

token zostaje po cichu pominięty w JSON. To zaleta, gdy pole zawiera prywatne dane, i niespodzianka, gdy pole miało trafić do wyniku, a ktoś zapomniał o wielkiej literze.

Podział pakietu na pliki

Pakiet może obejmować dowolnie wiele plików. Każdy plik w katalogu z tą samą linią package widzi każdą nazwę zadeklarowaną w pozostałych, eksportowaną czy nie, bez żadnego importowania:

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 wywołuje total bezpośrednio. Między plikami tego samego pakietu nie ma #include ani importów.

Błąd "undefined" po go run main.go

To najczęściej wyszukiwany problem z pakietami w Go i ma jedną przyczynę. Uruchomienie tylko pliku z main kompiluje tylko ten plik:

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

command-line-arguments to nazwa, jaką Go nadaje pakietowi zbudowanemu z listy plików, a math.go nie ma na tej liście. Uruchom zamiast tego cały pakiet:

go run .
5

To samo dotyczy go build main.go. Używaj go run . i go build (albo go build ./cmd/app), a problem się nie pojawi. Jeśli utrzymuje się przy go run ., sprawdź, czy oba pliki mają package main i czy żaden nie jest wykluczony przez ograniczenie budowania (//go:build) albo przyrostek _test.go.

Własne pakiety

Podkatalog twojego modułu to osobny pakiet. Importuj go przez ścieżkę modułu z go.mod plus katalog:

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

Tu ludzie potykają się o dwie rzeczy. Import to ścieżka modułu plus katalog, a nie ścieżka względna: "./greet" nie działa w trybie modułów. A prefix pozostaje niewidoczny dla main, bo zaczyna się małą literą; częścią API pakietu jest tylko Hello.

Pakiety internal

Katalog o nazwie internal ogranicza, kto może importować to, co jest w środku. Kod z shop/internal/store może zaimportować każdy pakiet, którego korzeń to shop/, i nic spoza niego. Inny moduł, który spróbuje, dostanie:

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

Używaj internal/ dla kodu, który chcesz współdzielić między własnymi pakietami, nie obiecując go reszcie świata. Możesz swobodnie zmieniać jego API, bo żaden zewnętrzny kod nie może od niego zależeć.

Aliasy importów

Nadaj importowi inną lokalną nazwę, wpisując ją przed ścieżką. Typowy powód to dwa pakiety o tej samej nazwie:

Oba pakiety nazywają się rand, więc co najmniej jeden potrzebuje aliasu. Używaj aliasów przy konfliktach i przy niejasnych nazwach; nie zmieniaj nazw pakietów tylko po to, żeby je skrócić, bo czytelnicy znają strconv, a nie twoje sc.

Importy puste i z kropką

Import pusty (_). Importuje pakiet wyłącznie dla efektów ubocznych: wykonują się jego zmienne pakietowe i funkcje init, ale nie używasz żadnej z jego nazw. W ten sposób rejestrują się sterowniki baz danych i dekodery obrazów:

import (
	"database/sql"

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

Bez _ taki import byłby błędem nieużywanego importu.

Import z kropką (.). Umieszcza eksportowane nazwy pakietu bezpośrednio w zasięgu twojego pliku, więc piszesz Println zamiast fmt.Println. Ukrywa, skąd pochodzą nazwy, i odradza się go poza kilkoma wzorcami pomocników testowych.

Cykle importów

Go nie pozwala, żeby dwa pakiety importowały się nawzajem, bezpośrednio albo przez łańcuch:

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

Błąd wypisuje cały łańcuch. Prawie każdy przypadek rozwiązuje jedna z dwóch poprawek:

  1. Przenieś kod potrzebny obu pakietom do trzeciego pakietu niższego poziomu, który importują oba.
  2. Jeśli pakiet A musi tylko wywołać coś z B, zdefiniuj mały interfejs w A i pozwól, żeby typ z B go spełniał. A nie importuje już B.

Cykl zwykle oznacza, że granice pakietów przebiegają przez warstwy kodu, które w rzeczywistości należą do siebie, więc to także sygnał, żeby przemyśleć podział.

Częste błędy

  • Uruchamianie pojedynczego pliku. go run main.go w pakiecie z wieloma plikami: użyj go run ..
  • Importy względne. import "./utils" nie działa w trybie modułów. Użyj "example.com/yourmodule/utils".
  • Nazwy małą literą, które miały być eksportowane. Funkcja, pole albo metoda, których potrzebują inne pakiety, musi zaczynać się wielką literą.
  • Nazwa pakietu niezgodna z katalogiem. Dozwolone, ale mylące: ścieżka importu mówi utils, a kod helpers.X. Trzymaj je takie same.
  • Nazwy pakietów typu worek na wszystko. util, common i misc nic nie mówią o zawartości i zwykle przyciągają cykle importów. Nazywaj pakiety od tego, co dostarczają: money, auth, slug.

Najczęściej zadawane pytania

Dlaczego Go twierdzi, że funkcja z tego samego pakietu jest undefined?

Prawdopodobnie uruchamiasz go run main.go. Podanie nazwy pliku kompiluje tylko ten plik, więc brakuje funkcji zdefiniowanych w innych plikach tego samego pakietu: undefined: total. Uruchom zamiast tego go run ., co kompiluje każdy plik .go w katalogu. Sprawdź też, czy każdy plik ma tę samą linię package.

Jak dodać pakiet fmt w Go?

fmt należy do biblioteki standardowej i jest dostarczany razem z Go, więc nie trzeba niczego instalować. Dodaj import "fmt" po linii package i wywołuj jego funkcje z prefiksem fmt., np. fmt.Println("hi"). To samo dotyczy strings, os, time, net/http i każdego innego pakietu standardowego.

Jak zaimportować własny pakiet w Go?

Zaimportuj go przez ścieżkę modułu plus katalog. W module o nazwie example.com/shop pakiet z folderu greet importuje się jako import "example.com/shop/greet" i używa jako greet.Hello(). Dla importującego widoczne są tylko nazwy zaczynające się wielką literą.

Czym różnią się nazwy eksportowane i nieeksportowane w Go?

Nazwa zaczynająca się wielką literą (Hello, Total, User.Name) jest eksportowana i można jej używać z innych pakietów. Nazwa pisana małą literą (hello, total, user.name) nie jest eksportowana i jest widoczna tylko wewnątrz swojego pakietu. Nie ma słów kluczowych public ani private.

Jak naprawić błąd "import cycle not allowed" w Go?

Dwa pakiety importują się nawzajem, bezpośrednio albo przez łańcuch, czego Go zabrania. Przenieś współdzielony kod do trzeciego pakietu, który importują oba, albo zdefiniuj mały interfejs w pakiecie, który potrzebuje danego zachowania, żeby nie musiał już importować drugiego.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ