- Wstęp
- Instalacja
- Inicjacja projektu
- Biblioteka standardowa
- CLI
- Funkcja główna
- Kompilacja
- Podstawy
- Wskaźniki
- Deklarowanie typów
- Bloki
- Funkcje
- Struktury
- Metody
- Interfejsy
- Obsługa błędów
Wstęp
Go
lub często też zwany jako Golang
, to jest programowania od Google rozwijany od 2007 roku.
Autorzy języka chcieli połączyć co najlepsze w C i Python’nie oraz wykorzystać zalety nowoczesnych wielowątkowych procesorów.
Go jest łatwym, kompilowanym i statycznie typowanym językiem, którego składania zawiera tylko 25 keyword’ów
Do wersji 1.20 mamy::
break default func interface select
case defer go map struct
chan else goto package switch
const fallthrough if range type
continue for import return var
Instalacja
By zainstalować Go na Ubuntu wystarczy, wykonać:
cd ~/Downloads
wget https://go.dev/dl/go1.20.linux-amd64.tar.gz
sudo tar -C /usr/lib/ -xzf go1.20.linux-amd64.tar.gz
echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc && source ~/.bashrc
Następnie możemy wywołać komendę go
:
$ go
Go is a tool for managing Go source code.
Usage:
go <command> [arguments]
The commands are:
bug start a bug report
(...)
vet report likely mistakes in packages
Use "go help <command>" for more information about a command.
Additional help topics:
buildconstraint build constraints
buildmode build modes
c calling between Go and C
cache build and test caching
environment environment variables
filetype file types
go.mod the go.mod file
gopath GOPATH environment variable
gopath-get legacy GOPATH go get
goproxy module proxy protocol
importpath import path syntax
modules modules, module versions, and more
module-get module-aware go get
module-auth module authentication using go.sum
packages package lists and patterns
private configuration for downloading non-public code
testflag testing flags
testfunc testing functions
vcs controlling version control with GOVCS
Use "go help <topic>" for more information about that topic.
Inicjacja projektu
Projekt rozpoczynamy albo z poziomu shell’a:
cd repos && mkdir example && go mod init example && touch main.go
Albo z pomocą IDE, np. Goland’a.
Biblioteka standardowa
Go posiada bogatą bibliotekę standardową, dzięki której w teorii bez korzystania z jakiegokolwiek framework’u - jesteśmy w stanie napisać każdą aplikację.
Dokumentacja std znajduje się pod tym linkiem.
CLI
Jak już wspomniałem standardowe CLI dostępne jest via go
:
go
The commands are:
bug start a bug report
build compile packages and dependencies
clean remove object files and cached files
doc show documentation for package or symbol
env print Go environment information
fix update packages to use new APIs
fmt gofmt (reformat) package sources
generate generate Go files by processing source
get add dependencies to current module and install them
install compile and install packages and dependencies
list list packages or modules
mod module maintenance
work workspace maintenance
run compile and run Go program
test test packages
tool run specified go tool
version print Go version
vet report likely mistakes in packages
Funkcja główna
Główna funkcja jest deklarowana w sposób podobnych do innych języków jak Kotlin, C:
package main
func main() {
}
Importy
Importowanie zależności jest podobne do innych języków:
// main.go
package main
import "store"
...your code
// Package store
package store
... your code
Kompilacja
Do tzw. kompilacji wraz z rozruchem możemy użyć:
go run main.go
Do zwyczajnej kompilacji, używamy:
go build main.go
W celu kompilacji do specyficznej architektury, możemy użyć:
GOOS=windows GOARCH=amd64 go build main.go -o bin/app.exe
GOOS=linux GOARCH=amd64 go build main.go -o bin/app
By wyświetlić wszystkie możliwe targety, należy użyć komendy:
go tool dist list
aix/ppc64 freebsd/amd64 linux/mipsle openbsd/386
android/386 freebsd/arm linux/ppc64 openbsd/amd64
android/amd64 illumos/amd64 linux/ppc64le openbsd/arm
android/arm js/wasm linux/s390x openbsd/arm64
android/arm64 linux/386 nacl/386 plan9/386
darwin/386 linux/amd64 nacl/amd64p32 plan9/amd64
darwin/amd64 linux/arm nacl/arm plan9/arm
darwin/arm linux/arm64 netbsd/386 solaris/amd64
darwin/arm64 linux/mips netbsd/amd64 windows/386
dragonfly/amd64 linux/mips64 netbsd/arm windows/amd64
freebsd/386 linux/mips64le netbsd/arm64 windows/arm
Więcej możecie znaleźć tu oraz tu.
Podstawy
Deklaracja zmiennych
Do deklarowania zmiennych używamy var
, natomiast dla stałych consts
:
var foo string
var bar string = "I am an initialized variable."
quaz := "I am short hard declared variable."
Typy danych
W Go
możemy odnaleźć między innymi takie typy danych:
package main
//String
var thisIsTheString string = "I am a string"
// Bool
var isItTrue bool = true
var isItRobot bool = false
// Numeric types
var i int = 444 // Platform dependent
var i8 int8 = 64 // -128 to 127
var i16 int16 = 32767 // -2^15 to 2^15 - 1
var i32 int32 = -2147483647 // -2^31 to 2^31 - 1
var i64 int64 = 9223372036854775807 // -2^63 to 2^63 - 1
var ui uint = 404 // Platform dependent
var ui8 uint8 = 255 // 0 to 255
var ui16 uint16 = 63535 // 0 to 2^16
var ui32 uint32 = 2223483647 // 0 to 2^32
var ui64 uint64 = 9223372036854666666 // 0 to 2^64
var uiptr uintptr // Integer representation of a memory address
// Floats
var fl32 float32 = 1.1333
var fl64 float64 = 33.5555
// Complex
var compl128 complex128 = 20 + 2
var compl64 complex64 = 10 + 33
// Array -> [n]T
var arry [5]int // where 5 is a length
var arr [4]string = [4]string{"foo", "bar", "car"}
var arrx [5]int = [5]int{3, 6, 11, 9, 99}
// Slices -> []T
var arrx []string
var s []int = []int{0, 1, 2, 3, 4, 5}
// Maps -> map[K]V
var receipts map[string]int
invoices := make(map[string]int)
documents := map[string]float64{
"ZS123405/500/2023/12": 5600.12,
"INV00033/322/2022/01": 999312.87,
"COR00231/001/2016/11": 123.93
}
W przypadku integerów zalecanym jest by używać int
** jeśli nie ma szczególnego powodu do używania konkretnej precyzji.
Typy byte
(uint8
) i rune
(int32
), są generalnie aliasami.
type byte = uint8
type rune = int32
Konwersja typów
Typy danych możemy konwertować w znanym z innych imperatywnych języków stylu:
var inty int = 42
var f = float64(inty)
Wartości zerowe
W przeciwieństwie do innych języków jak np. Python i Ruby, niezainicjowane zmienne otrzymują domyślną wartość zerową
.
Dla liczb jest to0
, a dla boolean’ów false
, natomiast dla stringów ""
.
Printowanie wartości
Do printowania używamy biblioteki fmt
:
var x string = "Hello <3"
func main () {
fmt.Println("x")
}
Zmienne mozemy osadzać w tekście tak:
var x string = "Mike"
func main () {
fmt.Printf("Hello: %s", x)
}
Mały cheat-sheet dla mapowania:
bool: %t
int, int8 etc.: %d
uint, uint8 etc.: %d, %#x if printed with %#v
float32, complex64, etc: %g
string: %s
chan: %p
pointer: %p
Widzialność importów
W celu eksportu metod czy struktur albo funkcji, ta zdefiniowana musi być z pomocą dużej litery.
Wartości, metody i zmienne z tzw małej litery uznawane są za nieeksportowalne
.
Wskaźniki
Wskaźniki mają identyczne zastosowanie jak w C, tj jeżeli zadeklarujemy zmienną wskazującą wskaźnik innej wartości:
var x int = 1
p := &x
*p = 2
fmt.Println(x)
fmt.Println(p)
fmt.Println(*p)
To p
zwróci adres w pamięci dla x
, *p
wartość.
Deklarowanie typów
W Go
, możemy tworzyć właśne typy danych:
// This is tempconversion package
package tempconversion
type Celsius float64
type Fahrenheit float64
const (
AbsoluteZeroC Celsius = -273.15
FreezingC Celsius = 0
BoilingC Celsius = 100
)
func Fahrenheit(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }
func FahrenheitToCelsius(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }
package main
import "tempconversion"
import "fmt"
func main() {
}
Bloki
If/else
Pętla if/else
jest podobna jak w innych językach.
Prosty if
:
func main() {
var foo string = "xd"
if foo == "abc" {
fmt.Println("Correct")
}
}
Inny przykład:
func main() {
var numbs int = 666
if numbs <= 333 {
fmt.Println("Number is too low...")
} else if numbs > 334 && <= 665 {
fmt.Println("Number is kinda close...")
} else if numbs >= 667 {
fmt.Println("Seek lower...")
} else {
fmt.Println("Welcome to HELL.")
}
}
Skrócony if
:
func main() {
if x:= 555; x > 444 {
fmt.Println("Variable 'x' is greater than 444...")
}
}
W Go do wersji 1.20 nie było tzw. ternary operator
(np. jak Pythonie: min = a if a < b else b
).
Switch
Przykładowy switch/case
w Go
:
func main() {
day := "wednesday"
switch day {
case "monday":
fmt.Println("Back to Mordor...")
case "friday":
fmt.Println("It's weekend finally...")
default:
fmt.Println("I don't care.")
}
}
Alternatywna wersja:
func main() {
day := "monday"
switch day {
case "monday":
fmt.Println("Back to Mordor...")
fallthrough
case "friday":
fmt.Println("It's weekend finally...")
default:
fmt.Println("I don't care.")
}
}
Uwaga; fallthrough
używany jest do przetransferowania kontroli do następnego case’a nawet gdy obecny case spełnia warunek.
Pętle for
Podstawowa pętla for
:
func main() {
for i := 0; i < 10; i++ {
fmt.Println(i)
}
}
Inny przykład, for/range
:
var arr [4]string = [4]string{"a", "b", "c"}
var arrx [5]int = [5]int{3, 6, 8, 9, 99}
func iterate(fw [4]int, fwx [5]int) {
// Values
for _, v := range fw {
fmt.Printf("%d\n", v)
}
// Keys and values
for i, v := range fwx {
fmt.Printf("%d %d\n", i, v)
}
}
var runes []rune
for _, r := range "Hi, Mike" {
runes = append(runes, r)
}
fmt.Printf("%q\n", runes) // "['H' 'i' ',' ' ' ' M' 'i' 'k' 'e']"
func equal(x, y map[string]int) bool {
if len(x) != len(y) {
return false
}
for k, xv := range x {
if yv, ok := y[k]; !ok || yv != xv {
return false
}
}
return true
}
Funkcje
Podstawy
Funkcję w Go tworzymy jak poniżej:
func name(param type) (result type) {
function body
}
Bardziej realistyczny przykład:
func receiptValue(sale float64, tax float32) float64 {
fmt.Println(sale, tax)
return sale + (sale * float64(tax))
}
func main() {
fmt.Println(receiptValue(100.00, 0.19))
}
W przypadku gdy wszystkie parametry mają ten sam typ, używamy:
func myFunc(z, x, c, v string) string {}
Rekurencja
Przykład rekurencji w Go. Która w przypadku algorytmów, które operują na danych na danych może osiągać benefity z korzystania ze stosu np. FILO (First In Last Out).
func recursiveFunction(number int) int {
if number < 6 {
return number
}
return number + recursiveFunction(number - 1)
}
func main() {
calc := recursiveFunction(6)
fmt.Printf("Recursive: %d", calc)
}
Funkcje jako zmienne
Funkcję mogą być przypisane do zmiennych:
func myFunction() {
val := myInsideFunc() {
fmt.PrintLn("I am inside function.")
}
val()
}
Domknięcia
Domknięcie, znane również jako leksykalne domknięcie lub funkcja blokująca, to funkcja, która ma dostęp do zmiennych z otaczającego ją kontekstu, w którym została zdefiniowana. Domknięcia “pamiętają” te zmienne, nawet po zakończeniu wykonania otaczającej funkcji.
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
Zwracanie wielu wartości
Go
wspiera zwracanie wielu wartości:
func myFunc(foo string) (string, float32) {
return foo, 666.0
}
var text, nmb = myFunc('boo')
Defer
Defer
to keyword który umożliwia opóźnienie egzekucji funkcji dopóki dopóki otaczająca go funkcja nie zakończy swojego działania:
func main() {
defer fmt.Println("I am done!")
fmt.Println("Deploying package...")
}
Funkcje wariadyczne
Funkcje wariadyczne (variadic functions), to funkcje, które przyjmują zero lub wiele argumentów (...
):
func add(values ... int) int {
sum := 0
for _, v := range values {
sum += v
}
return sum
}
Wskaźniki jako argumenty funkcji
Wskaźniki
mogą być używane jako argumenty funkcji:
func myFunction(p *int) {}
Struktury
Jeśli przychodzisz z języka obiektowego jak Ruby albo Python, to możesz pomyśleć o strukturach jak o lekkich klasach.
Struktury
pozwalają na kompozycję, ale nie wspierają dziedziczenia (tam samo jak w C).
Struktury definiujemy jak poniżej:
type Store struct {
Code string
Name string
Warehouse int
}
Tworzenie obiektu na podstawie struktury obiektu:
func main() {
var s1 = Store{
Code: "X0001",
Name: "Warsaw",
Warehouse: 33,
}
var s2 = Store{"X0002", "Gdansk", 99}
fmt.Println("Store #1:", s1)
fmt.Println("Store #2:", s2)
}
Note: Wszystkie pola muszą być wypełnione. Inaczej kod się nie skompiluje.
Dostęp do wartości w obiektach
Zwracanie wartości z obiektów jest proste i intuicyjne:
type Warehouse struct {
Code int
Town string
}
func main() {
var wr1 = Warehouse{
Code: 33,
Town: "Berlin",
}
fmt.Println(wr1.Code)
fmt.Println(wr1.Town)
}
Struktury i wskaźniki
W przypadku struktur, też możemy używać wskaźników:
type Warehouse struct {
Code int
Town string
}
func main() {
var wr1 = Warehouse{
Code: 33,
Town: "Berlin",
}
wh := &wr1
fmt.Println(wr1.Code)
fmt.Println((*wh).Town)
}
Widoczność atrybutów
Atrybuty zaczynające się tylko z dużej litery, są widoczne po za package’m:
type Member struct {
FirstName, LastName string
Age int
placeOfBirth string
}
Structura Member
zostanie wyeksportowana, ale bez atrybutu placeOfBirth
.
Kompozycja
Jak już wspomniałem Go
nie wspiera dziedziczenia
, ale za to wspiera kompozycję
:
type Engine struct {
Fuel string
Power float32
Torque float32
}
type Locomotive struct {
Engine Engine
Axis int
}
Tags
W Go
istnieje termin struct tags
. Takie tagi używane są np przez liczne biblioteki enkodujące
i ORM'y
:
type Animal struct {
Name string `json:"name"`
Age int `json:"age"`
}
Metody
Do typów i struktur możemy definiować własne metody:
func (variable Type) MethodName(params) (returnTypes) {}
type Engine struct {
Fuel string
Power float32
Torque float32
}
type Locomotive struct {
Engine Engine
Axis int
}
type Train struct {
Traction Locomotive
CoachesType string
CoachesCount int
Destination string
}
func (t Train) IsPassenger() bool {
return t.CoachesType == "passenger"
}
Utworzenie obiektu oraz wywołanie metody:
func main() {
myEngine := Engine{"Diesel", 3400.00, 9000.00}
myLocomotive := Locomotive{myEngine, 6}
myTrain := Train{myLocomotive, "passenger", 16, "Warsaw"}
fmt.Println("Is passenger: ", myTrain.IsPassenger())
Metody i odbiorcy wskaźników
Weźmy np. taką metodę jak CoachesType
:
func (t Train) UpdateCoachesType(name string) {
t.CoachesType = name
}
Spróbujmy zmienić wartość:
func main() {
myEngine := Engine{"Diesel", 3400.00, 9000.00}
myLocomotive := Locomotive{myEngine, 6}
myTrain := Train{myLocomotive, "passenger", 16, "Warsaw"}
fmt.Println("Is passenger: ", myTrain.IsPassenger())
myTrain.UpdateCoachesType("freight")
fmt.Println(myTrain.CoachesType)
}
Jak widać wartość pozostała niezmieniona:
Is passenger: true
passenger
Bez obaw, jest to oczekiwany wynik z racji, że metody bez wskaźników nie aktualizują wartości.
Jednakże jeżeli zrobimy to tak:
func (t *Train) UpdateCoachesTypeProper(name string) {
t.CoachesType = name
}
func main() {
myEngine := Engine{"Diesel", 3400.00, 9000.00}
myLocomotive := Locomotive{myEngine, 6}
myTrain := Train{myLocomotive, "passenger", 16, "Warsaw"}
fmt.Println("Is passenger: ", myTrain.IsPassenger())
myTrain.UpdateCoachesType("freight")
fmt.Println(myTrain.CoachesType)
myTrain.UpdateCoachesTypeProper("freight")
fmt.Println(myTrain.CoachesType)
}
Wartość ulega zmianie!
Is passenger: true
passenger
freight
Interfejsy
Interfejsy w Go to kluczowy element języka, który umożliwia tworzenie abstrakcyjnych typów danych. Interfejs definiuje zestaw metod, które muszą być zaimplementowane przez typ, aby spełniać ten interfejs.
Interfejsy są używane do definiowania funkcji, które mogą być używane przez różne typy. Są one szczególnie przydatne w sytuacjach, gdy chcesz, aby funkcja mogła obsługiwać różne typy danych.
W Go, typ spełnia interfejs, jeśli implementuje wszystkie metody wymienione w definicji interfejsu. Nie jest wymagane jawnie stwierdzenie, że dany typ spełnia interfejs - to jest ustalane dynamicznie w czasie wykonania.
Przykładem może być interfejs io.Reader
z biblioteki standardowej Go, który definiuje metodę Read
. Każdy typ, który implementuje metodę Read
z odpowiednim sygnaturą, spełnia interfejs io.Reader
. To pozwala na użycie różnych typów, takich jak pliki, sieci, buforów itp., w sposób generyczny, gdzie jest wymagany io.Reader
.
Przykład interfejsu Vehicle oraz typów Car, Lorry oraz Tractor:
package main
import "fmt"
type Vehicle interface {
StartEngine() string
}
type Car struct {
Brand string
}
type Lorry struct {
Brand string
}
type Tractor struct {
Brand string
}
func (c Car) StartEngine() string {
return fmt.Sprintf("%s's car engine started", c.Brand)
}
func (l Lorry) StartEngine() string {
return fmt.Sprintf("%s's lorry engine started", l.Brand)
}
func (t Tractor) StartEngine() string {
return fmt.Sprintf("%s's tractor engine started", t.Brand)
}
func main() {
car := Car{"Toyota"}
lorry := Lorry{"Volvo"}
tractor := Tractor{"John Deere"}
vehicles := []Vehicle{car, lorry, tractor}
for _, vehicle := range vehicles {
fmt.Println(vehicle.StartEngine())
}
}
Oto przykład interfejsu, struktury i metody w Go, które używają Car, Lorry i Tractor:
package main
import "fmt"
// Vehicle jest interfejsem, który definiuje metody, które muszą być zaimplementowane przez każdy typ pojazdu
type Vehicle interface {
StartEngine() string
}
// Car jest strukturą reprezentującą samochód
type Car struct {
Brand string
}
// Lorry jest strukturą reprezentującą ciężarówkę
type Lorry struct {
Brand string
}
// Tractor jest strukturą reprezentującą traktor
type Tractor struct {
Brand string
}
// StartEngine jest metodą struktury Car, która implementuje interfejs Vehicle
func (c Car) StartEngine() string {
return fmt.Sprintf("%s's car engine started", c.Brand)
}
// StartEngine jest metodą struktury Lorry, która implementuje interfejs Vehicle
func (l Lorry) StartEngine() string {
return fmt.Sprintf("%s's lorry engine started", l.Brand)
}
// StartEngine jest metodą struktury Tractor, która implementuje interfejs Vehicle
func (t Tractor) StartEngine() string {
return fmt.Sprintf("%s's tractor engine started", t.Brand)
}
func main() {
car := Car{"Toyota"}
lorry := Lorry{"Volvo"}
tractor := Tractor{"John Deere"}
vehicles := []Vehicle{car, lorry, tractor}
for _, vehicle := range vehicles {
fmt.Println(vehicle.StartEngine())
}
}
W tym kodzie mamy interfejs Vehicle
, który definiuje metodę StartEngine()
. Mamy też trzy struktury: Car
, Lorry
i Tractor
, które wszystkie implementują ten interfejs. W funkcji main
tworzymy instancje tych struktur i używamy ich do wywołania metody StartEngine()
.
Obsługa błędów
Język Go nie został wyposażony w mechanizm wyjątków błędów w stylu try .. catch
lub begin .. escape .. ensure
.
Go obsługuje błędy jędynie poprzez zwracanie wartości typu error
, zawsze jako ostatniej spośród wszystkich wartości zwracanych przez funkcję.
W każdym przypadku gdy funkcja coś zwraca - możesz przy pomocy funkcji sprawdzić, czy zmienna odpowiadająca za błędy ma wartość nil:
func calcRemainderAndMod(num, den int) (int, int, error) {
if den == 0 {
return 0, 0, errors.New("Denominator is 0")
}
return num / den, num % den, nil
}
func main() {
num := 20
den:= 3
remainder, mod, err := calcRemainderAndMod(num, den)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
fmt.Println(remainder, mod)
}