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()
    }

}