Pakiety
Go posiada mechanizm podziału programów na pakiety. Dzięki nim możemy odseparować logiczne całości aplikacji, wydzielać biblioteki i narzędzia.
Do omówienia mamy kilka kwestii:
- deklaracja i importowanie pakietu
- widoczność składowych pakietu
- kompilacja pakietów bibliotecznych
- testowanie pakietów
Deklaracja i importowanie pakietów
Deklaracja nazwy pakietu to zawsze pierwsza instrukcja w pliku źródłowym, inaczej
kod się nie skompiluje. Sama deklaracja jest prosta package i nazwa pakietu.
package main
Istnieją jednak pewne zwyczaje co do nazw. Są to rzeczowniki w liczbie pojedynczej pisane małymi literami. Jeśli pakiet mieści się w jednym pliku go, to nazwa pakietu powinna być taka sama jak nazwa pliku, bez rozszerzenia ".go". Każdy plik należący do pakietu powinien deklarować nazwę tegoż pakietu.
Jeśli importujemy jakieś pakiety do pliku źródłowego to musi się to odbyć tuż
pod deklaracją nazwy pakietu. Samo importowanie można wykonać na kilka sposobów.
Import pojedynczego pakietu ma następującą składnię: import identyfikator
"ścieżka do pakietu".
Identyfikator to ciąg znaków, którym będziemy odwoływali się do składowych pakietu dalej w kodzie. Identyfikator jest opcjonalny, domyślnie identyfikator importowanego pakietu to ostatni człon ścieżki. Można użyć także kropki jako identyfikatora, sprawi to import wszystkich identyfikatorów importowanego pakietu do zasięgu pliku
Ścieżka do pakietu, może być absolutna (od głównego katalogu na dysku), względem
katalogu $GOROOT/pkg/$GOOS_$GOARCH, lub względem bieżącego katalogu w którym
znajduje się kod (o ile zaczniemy od "./"), może być też ścieżką do repozytorium
na github, code.google.com, bazar itp; ujmujemy ją w cudzysłów.
uwaga wraz z wersją 2012-12-22 pojawia się nowe narzędzie do budowania i instalowania pakietów, dodana zostaje możliwość ustawienia zmiennej środowiskowej $GOPATH, która staje się bazową ścieżką dla wszystkich pakietów spoza biblioteki standardowej. @see{go cmd}
Przykładowe pojedyncze importy:
import "fmt" //pakiet fmt będzie widziany pod identyfikatorem fmt import format "fmt" //pakiet fmt będzie widziany pod identyfikatorem format import . "fmt" //wszystkie składowe pakietu, są widoczne w zasięgu pliku
Importy można grupować, wystarczy wziąć identyfikatory i ścieżki pakietów w nawiasie i rozdzielić średnikami lub nowymi liniami.
import (math, fmt, http) import ( math fmt http )
Widoczność składowych pakietów
To bardzo ważna kwestia! W go na zewnątrz pakietu są udostępnione tylko te identyfikatory które zaczynają się wielką literą. To nie żart, to jedyna metoda udostępniania zmiennych, typów czy funkcji poza pakiet. Co do zasady, możemy używać w dowolny sposób tylko tych identyfikatorów, które są pisane z wielkiej litery, co nie oznacza niespodziewanej dyskryminacji znaczenia identyfikatorów pisanych mała literą. Przeanalizujmy przykłady:
Udostępnianie typów
package example type secret int type Point struct { x, y int }
W programie importującym pakiet example możemy utworzyć wartość typu
example.Point ale już nie możemy utworzyć wartości typu secret. Co więcej
nie możemy zmodyfikować żadnego z pól x, y, bo są niewyeksportowane. Taka
operacja udała by się gdyby pakiet example udostępniał, funkcję lub metodę
umożliwiającą zmianę np.:
package example type Point struct { x, y int } func NewPoint(x,y int) *Point { return &Point{x,y} } func (p *Point) Set(x,y int) { p.x, p.y = x, y //tu mała "sztuczka" z jednoczesnymi przypisaniami }
Dodana została funkcja example.NewPoint tworząca zmienną typu *example.Point,
przyjmująca składowe x, y, które będą użyte w inicjacji wartości. Druga
funkcja to metoda typu example.Point: Set(x,y int) przyjmuje ona wartości,
które zostaną przypisane do wartości danego typu.
Na marginesie, gdy mamy taki zestaw funkcji, to w praktyce nie potrzebujemy by
sam typ example.Point był eksportowany, bo możemy wykonać wszystkie operacje,
za pomocą wyeksportowanych metod i funkcji, a tak naprawdę samo wyeksportowanie
typu mało daje.
Gdybyśmy zadeklarowali pola x i y wielką literą - jako X, Y, to były by
dostępne także w pakietach importujących.
Kompilacja pakietów
Wiem z doświadczenia, że to kłopotliwa sprawa i ciężko znaleźć dobry opis jak to robić, postaram się by ten był w miarę kompletny.
Pakiety są różne i możemy mieć potrzebę budowania ich na kilka sposobów.
Pakiety jedno plikowe
Najprostszym przypadkiem są pakiety, które mieszczą się w jednym pliku. Takie pakiety przeważnie wystarczy skompilować używając np. 6g (do wyboru w zależności od systemu są jeszcze 8g, 5g). Kod z ostatniego przykładu skopiujmy do pliku example.go. Wtedy wystarczy wpisać:
6g example.go
A cały program np. taki:
package main import ( "fmt" ex "./example" ) func main() { p := ex.NewPoint(1,1) fmt.Println(p) }
Zwróć uwagę na sposób importowania pakietu: ./example, wskazuje że skompilowany
pakiet będzie w tym samym katalogu co pakiet main programu. Wszystko pójdzie
dobrze jeśli skompilowany pakiet example będzie w pliku example.6 (rozszerzenie
jest zależne od kompilatora). Wtedy wystarczy normalnie skompilować program:
6g example.go
6g main.go && 6l -o program main.6
Opcja linkera -o instruuje do jakiego pliku ma zostać zlinkowany kod. Teraz
możemy spokojnie uruchomić program.
Gdybyś jednak miał potrzebę wskazania innego katalogu w którym jest skompilowany pakiet to masz dwa wyjścia - zmienić ścieżkę w imporcie, bądź ją wskazać w kompilacji.
Przykładowo plik pakietu example.go masz w katalogu example. Wtedy zmień
polecenie importu na następujące:
import ex "./example/example"
Teraz, o ile oczywiście skompilowałeś pakiet i masz w katalogu example plik
example.6, wystarczy wpisać
6g main.go && 6l -o program main.6
Drugi sposób także wymaga zmiany polecenia importu. Powinno wyglądać tak:
import ex "example"
A przy kompilacji programu dodajemy flagę -I dodającą katalog w którym są
szukane pakiety
6g -I ./example/ main.go && 6l main.go
Pakiety wieloplikowe
Generalnie zasady kompilacji pakietów wieloplikowy nie są różne od jedno plikowych. Po prostu przy kompilacji należy wskazać listę plików wchodzących w skład pakietu.
Załóżmy, że rozbiliśmy nasz mały pakiet na dwa pliki:
example.go:
package example type Point struct { x, y int } func (p *Point) Set(x,y int) { p.x, p.y = x, y }
example1.go:
package example func NewPoint(x,y int) *Point { return &Point{x,y} }
jego kompilacja jest prosta:
6g example.go example1.go
i po robocie :-) Należy pamiętać by wylistować wszystkie pliki
Makefile
Kompilacja na piechotę jak powyżej to trochę nużąca sprawa i robienie tego
ręcznie za każdym razem, może powodować błędy. Dlatego polecam zaprzyjaźnić
się z make i Makefile. Nie będę tłumaczyć jak się je konstruuje, napiszę
jak wykorzystać te Makefile które twórcy Go dostarczają w standardzie.
Aby skompilować pakiet używając make wystarczy dostosować następujący plik
Makefile do swoich potrzeb:
include $(GOROOT)/src/Make.inc TARG=example GOFILES=\ example.go\ example1.go\ include $(GOROOT)/src/Make.pkg
Uruchomienie make w katalogu z tym plikiem i plikami pakietu skompiluje go.
Utworzy się katalog _obj w którym znajdzie się plik example.a, to
zarchiwizowana postać pakietu, którą można przekopiować do katalogu z pakietami
dostępnymi globalnie i nie martwić się o ścieżki importu. Można to zrobić
wpisując make install.
Jest jeszcze jedno zastosowanie takiego Makefile za jego pośrednictwem można
uruchamiać testy, które napisaliśmy do pakietu, ale o tym w następnej sekcji.
Testowanie pakietów
Go w bibliotece standardowej ma pakiet testing a w nim m.in. wyeksportowany typ
testing.T, który ma kilka metod przydatnych do testowania takich jak Fail()
lub Error(). Nie ma metod "pozytywnych", ale metody negatywne wystarczają by
sprawdzać poprawność pakietu.
Pliki z testami nazywamy tak jak pakiet dodając sufix _test np. dla pakietu
example powinien nazywać się example_test.go. Testami są eksportowane funkcje
których nazwa zaczyna się od słowa Test, a jako argument przyjmują zmienną typu
*testing.t. Może być ich wiele. Przykładowy plik z testami może wyglądać tak:
package example import ( "testing" ) func TestNew(t *testing.T) { a := NewPoint(1,1) if a.x != 1 || a.y != 1 { t.Fail() } } func TestSet(t *testing.T) { a := NewPoint(1,1) a.Set(2,2) if a.x == 1 || a.y == 1 { t.Fail() } if a.x != 2 || a.y != 2 { t.Fail() } }
Wszystko o języku programowania Go