poniedziałek, października 04, 2010

Go dla Java'owca ;-) odcinek 1 "klasy"

Od kilku dni czytam sobie o Go, głównie rekreacyjnie, ale z tyłu głowy mam to, że może kiedyś będzie trzeba przejść na Go, albo C/C++ (co chyba bardziej prawdopodobne) po tym jak Oracle zaczyna powoli rozrabiać z Java'ą ;-) [a wolałem żeby Suna kupił Oracle niż IBM... ;-)]

Pierwszym szokiem dla Java'owca będzie to, że choć Go jest językiem obiektowym [także!] to nie ma tam klas!

Ale nie ma obaw, spokojnie możemy dodawać metody do typów danych. Jedyny wymóg by typ danych był w tym samym pakiecie co metody.

Java'owy kod w stylu:
public class Point {
private int x, int y;
public Point(int x,int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public float distanceFrom00() {
return Math.sqrt(x*x+y*y);
}
}

W Go wygląda mniej więcej tak:
type Point struct {
x int;
y int;
}
func (p *Point) GetX() int {
return p.x
}
func (p *Point) GetY() int {
return p.y
}
func (p *Point) DistanceFrom00() float {
return float(math.Sqrt(float64(p.x*p.x)+float64(p.y*p.y)))
}

Czyli definiujemy nowy typ danych, a później metody dla tego typu.
Wołanie metod jest dokładnie takie samo jak w Java'ie czyli po kropce.
Ze złych wieści, tu znów są wskaźniki :-( Na szczęście język trochę pomaga, i np. w deklaracjach metod nie trzeba wcale robić (p *Point) można zrobić (p Point).
To p to receiver, który robi za odpowiednik this.
Stąd w getterach jest użyte p.x.

Dziedziczenie jest, choć ciut inaczej zorganizowane.
Jeżeli np. stworzymy nowy typ Point3, w którym dodamy jeszcze z, to mamy wtedy:
type Point3 struct {
Point
z int
}

Trzeba doimplementować jeszcze GetZ() i nadpisać DistanceFrom00(), co wygląda tak:
func (p *Point3) GetZ() int {
return p.z
}

func (p *Point3) DistanceFrom00() float {
return float(math.Sqrt(math.Pow(float64(p.Point.DistanceFrom00()),2)+float64(p.z*p.z)))
}


Gorzej jest z tworzeniem tych obiektów :-(
p:=Point{3,2} // tworzy strukturę z x=3 i y=2
p3:=Point3{Point{3,2},1} // tworzy Point3 z x=3,y=2,z=1.......
p4:=new(Point) // tworzy nowy Point, w p4 jest wskaźnik do niego z x=0, y=0

Jak widać w tym fragmencie jest to ohydne ;-) dlatego jak rozumiem zaleca się tworzenie metod fabrykujących, które będą zwracały nasze obiekty.

Teraz ważna rzecz, w Go nie ma modyfikatorów dostępu takich jak w Java'ie, nie ma tu public, private czy protected, jest jednak sposób na ograniczanie widoczności.
Jeżeli nazwa pola, typu, czy metody jest pisana dużą literą to takie coś będzie widoczne poza swoim pakietem, wszystko pisane z małej litery widoczne jest tylko w pakiecie.
To wydaje się mieć sens, w Java'ie ewidentnie programiści przynieśli zwyczaje z C++ i to co było pomyślane jako mechanizm namawiający do używania modyfikatorów dostępu tak by to pakiety były głównymi strukturami decydującymi o prywatności stało się mechanizmem do prywatyzowania na poziomie klas.

Jak było widać wyżej przez to jak się "dziedziczy", można dość łatwo wprowadzić wielodziedziczenie, z tym, że jak 2 "dziedziczone" typy mają pola czy metody o tej samej nazwie to się robią problemy i puki ich nie używamy to jest dobrze, ale jak użyjemy to jest błąd [nie przyglądałem się temu jeszcze więc wieżę na słowo temu co czytałem ;-)].

W Go jest za to coś takiego jak interfejsy :-) Ale działają one kompletnie inaczej niż w Java'ie, w każdym bądź razie w sferze deklarowania tego czy się je implementuje.....
Jeśli coś ma odpowiednie metody to implementuje dany interfejs i nie trzeba niczego deklarować!
Popatrzmy na kod:
type Printable interface {
String() string
}
type TosterInt struct { value int }
type TosterFloat struct { value float }
func (i TosterInt) String() string {
return fmt.Sprintf("%d",i.value)
}
func (f TosterFloat) String() string {
return fmt.Sprintf("%f",f.value)
}

I teraz wszędzie w kodzie możemy używać cosiów stworzonych z TosterInt i TosterFloat tak jakby były typu Printable :-) Czyli trzymać je np. w mapie gdzie wartości są Printable.
  m:=make(map[string]Printable)
m["TosterInt"]=TosterInt{3}
m["TosterFloat"]=TosterFloat{3.9}
for k,v:=range m {
fmt.Println(k," ",v)
}


Aha, tak biegiem, podstawowy program w Go wygląda tak
package main

import "fmt" // tutaj importujemy co nam potrzebne, zwykle potrzebujemy ;-)

func main() {
// nadziewka
var i = 1 // samo nam stworzy inta o wartości 1
var f float64 = 1.0 // samo nam stworzy float64 o wartości 1
i1:=1 // to samo co var i1=1
fmt.Println(i," ",f," ",i1)
}

W Go można przypisywać jednym := kilka wartości, funkcje też mogą zwracać kilka wartości np:
package main

import "fmt"

func toster() (string, int, bool) {
return "string",1,false
}


func main() {
a,b,c:=toster()
fmt.Println(a," ",b," ",c)
}

Wypisze "string 1 false".
To wieloprzypisywanie trochę na początku przeraża ;-)

Ok, to tyle, jak będę miał nastrój na dalszą naukę to napiszę więcej ;-)
Na razie zachęcam do przyjrzenia się tutorialowi [szczególnie prezentacji do której linki są na stronie tutoriala].

Nie jestem już tak obrzydzony do Go jak rok temu, ale nadal nie jest to mój ulubieniec ;-)
http://www.blogger.com/img/blank.gif
Podoba mi się to, że jak na razie wydaje się nie skupiać na "architekturze", nie muszę myśleć o hierarchii klas, nie muszę pilnować ciągle by wszystko co powinno dziedziczyło/implementowało to co powinno, to się tu samo wydaje robić. Potrzebuję metody to ją po prostu dopisuję......

Na razie jednak nie wychodzi mi komplikacja z kilku plików ;-) Używam portu dla Windows, o którym tu już kiedyś pisałem i niestety nie potrafi on chyba do końca pracować z wieloma plikami [jeżeli stworzy się swój własny pakiet], dodatkowo gdy program zrobi out of memory to pod Windows cały komputer zawisa, i po jakichś 30 sekundach program po prostu kończy swoje działanie, bez żadnej informacji........

Do prostych zabaw warto użyć umieszczonego na stronie Go placu zabaw ;-)


Podobne postybeta
Wysyłamy pliki do Google Docs przy pomocy Go :-)
Język Go dla Windows :-)
Go dla Java'owca ;-) odcinek 2 "kontenery dwa ;-)"
Go wolniejsze od C i JavaScript, i ciut szybsze niż Java ;-) [a jednak od Java'y też wolniejsze]
clone() i Cloneable się mszczą ;-)

13 komentarzy:

  1. Dzięki za tutoriala, językowi miałem ochotę przyjrzeć się już od dawna, ale wiecznie brakuje czasu.

    Wiele elementów z Go mi się bardzo podoba. Głównie podejście do obiektów, closures i ogólnie mniejsza sztywność języka..

    ps.
    Zwracanie więcej niż jednej wartości pamiętam z Lua - to bardzo pożyteczna cecha, poprawia zarówno czytelność jak optymalizację kodu.

    OdpowiedzUsuń
  2. Parę małych spostrzeżeń...

    Nie musisz pisać:
    type TosterInt struct { value int }

    możesz równie dobrze:
    type TosterInt int;

    To jest właśnie urok Go,
    obiekty nie są już jedynie strukturami na dopalaczach jak w C++ i Java.

    Warto także pamiętać że metody deklarowane dla typu nie są wirtualne. W tym celu trzeba używać interfejsów. Osobiście takie podejście wydaje mi się bardzo klarowne, ale programistę przyzwyczajonego do Java, gdzie wszystkie metody są wirtualne, takie coś może zaboleć.

    Puki co najmniej podoba mi się konieczność pisania publicznych nazw z dużej litery. Boję się że przy większym programie połamał bym sobie palce o shift.

    OdpowiedzUsuń
  3. @Red - ale wtedy się coś kaszani jak używa się Printable, są jakieś rzeczy w stylu out of memory [gdy buduje nowy typ na int, bo tak zrobiłem to za pierwszym razem :-)]
    Tego z wirtualnością nie sprawdzałem, ale pewnie masz rację, trzeba będzie sprawdzić :-)

    OdpowiedzUsuń
  4. Może jakiś błąd w kodzie lub kompilatorze?
    Mój przykład działa, może różnice naprowadzą Cię na odpowiedź?

    package main
    import "fmt"

    type Toster int;
    type Grill float;
    type Printable interface { Print() }

    func (t Toster) Print() { fmt.Println("toasted:",t) }
    func (t Grill) Print() { fmt.Println("bricked:", t) }

    func main() {
    var (
    t1 Toster = 1;
    t2 Grill = 3.14;
    p Printable;
    );

    p = t1;
    p.Print();

    p = t2;
    p.Print();
    }

    OdpowiedzUsuń
  5. @Red ja próbowałem czegoś takiego:


    package main
    import "fmt"

    type Toster int;
    type Grill float;
    type Printable interface { String() string }

    func (t Toster) String() string { return fmt.Sprint("toasted:",t) }
    func (t Grill) String() string { return fmt.Sprint("bricked:", t) }

    func main() {
    var (
    t1 Toster = 1;
    t2 Grill = 3.14;
    p Printable;
    );

    p = t1;
    fmt.Println(p);

    p = t2;
    fmt.Println(p);
    }


    i w Go Playground dostaję:
    mmap: errno=0xc
    throw: mmap

    panic PC=0x7fff6031bda0
    throw: malloc/free - deadlock
    double panic

    Jak zmienię String() na Print() i zamiast fmt.Println(p) zrobię fmt.Println(p.Print()) to działa :-)
    Ale zamiana ostatnich 4 linii na:
    p = t1;
    fmt.Println(p.String());

    p = t2;
    fmt.Println(p.String());

    też powoduje ten sam błąd.
    To pewnie coś podstawowego, ale nie wiem do końca co :-)

    OdpowiedzUsuń
  6. Chyba wiem... :)

    Winne jest wywołanie fmt.Sprint(t)
    ono wywołuje metodę String() no i tak w kółko.

    OdpowiedzUsuń
  7. To wiedząc to co ustaliłeś ten kod można tak poprawić (dokonać konwersji naszych nowych typów na oryginalne):


    package main
    import "fmt"

    type Toster int;
    type Grill float;
    type Printable interface { String() string }

    func (t Toster) String() string { return fmt.Sprint("toasted:",int(t)) }
    func (t Grill) String() string { return fmt.Sprint("bricked:", float(t)) }

    func main() {
    var (
    t1 Toster = 1;
    t2 Grill = 3.14;
    p Printable;
    );

    p = t1;
    fmt.Println(p);

    p = t2;
    fmt.Println(p);
    }

    OdpowiedzUsuń
  8. Właśnie to samo miałem napisać :)

    Jak zupełnie wywalić definicję interfejsu Printable przykład nadal będzie działał. Ducktyping...

    Szkoda tylko że kompilator tworzy statyczne binarki. 898K to trochę dużo, mam nadzieję że to można zmienić.

    OdpowiedzUsuń
  9. Tak będzie działał, ale tylko dlatego, że akurat String() jest "standardową" tak jak toString() w Java'ie.
    Mnie chodziło w przykładzie o pokazanie właśnie ducktypingu, a wtedy ten interfejs jest potrzebny bo przypisujemy nasze "obiekty" do zmiennej typu naszego interfejsu.
    W Twoim przykładzie Print lepiej działa bo wtedy bez Printable nie zadziała ;-)

    OdpowiedzUsuń
  10. Na razie nie da się tego zmienić. Jeśli mowa o Windowsowym ;-) Choć pewnie można spróbować użyć jakiegoś stripa.

    OdpowiedzUsuń
  11. Rozmiar podałem już po zestripowaniu (Linux). Różnica nie jest duża. Mogę łatwo się domyśleć że większość to GC i odpowiednik libc.

    Pisząc o wywaleniu definicji interfejsu Printable, miałem na myśli to że pakiet fmt już takową posiada.
    Trochę pogrzebałem, nazywa się to "Stringer" i poza nazwą wygląda dokładnie tak samo.

    OdpowiedzUsuń
  12. Trudno orzec.
    Ja i tak używam nieoryginalnego ;-) kompilatora dla Windows, bo jakoś nie chce mi się włączać VMWare tylko po to by się Go pobawić ;-)

    Rozumiem. Z Printable i bez widać ducktyping, z Printable "bardziej" bo nie implementując jawnie interfejsu się go spełnia. Lepiej wtedy zrobić go tak jak Ty zrobiłeś.
    Mowa ciągle o walorach edukacyjnych :-)

    OdpowiedzUsuń