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 $GOPATH oraz $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.

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
import "io/ioutil" //pakiet io/ioutil będzie widoczny pod symbolem ioutil

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. 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, 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.

Większość tej sekcji trąci myszką są już lepsze narzędzia do kompilacji niż to co poniżej

Pakiety jednoplikowe

Najprostszym przypadkiem są pakiety, które mieszczą się w jednym pliku. Takie pakiety przeważnie wystarczy skompilować używając narzędzia 6g (w zależności od systemu może być to 8g, 5g). Kod z ostatniego przykładu skopiujmy do pliku example.go. Wtedy wystarczy wpisać:

go tool 6g example.go

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:

go tool 6g example.go
go tool 6g main.go && go tool 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ć

go tool 6g main.go && go tool 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

go tool 6g -I ./example/ main.go && go tool 6l main.go

Pakiety wieloplikowe

Generalnie zasady kompilacji pakietów wieloplikowych nie są różne od jednoplikowych. 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:

go tool 6g example.go example1.go

i po robocie :-) Należy pamiętać by wylistować wszystkie pliki

Kompilacja za pomocą narzędzia go build

Testowanie pakietów

Go w bibliotece standardowej jest 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()
    }

}

Aby dowiedzieć się jak uruchamiać testy