Go (lang)

Go est le langage créé par Google. Il rassemble aussi des concepts intéressants, comme Rust, et il est également compilé.

Structure(s)

Packages

L’instruction import permet la factorisation, à savoir le regroupement des importations en une seule commande (avec des parenthèses). C’est moche.

Plus important : seuls les noms commençant par une majuscule sont exportés, et donc visibles en dehors du package. Là aussi c’est moche. Mais, logiquement, les fonctions exposées commenceront donc par une majuscule, comme fmt.Println().

Types de base

bool

string

int  int8  int16  int32  int64
uint uint8 uint16 uint32 uint64 uintptr

byte // alias for uint8

rune // alias for int32
     // represents a Unicode code point

float32 float64

complex64 complex128

Fonctions

Il existe une instruction intéressante que je n’ai retrouvée dans aucun autre langage : defer. Cela permet l’exécution à la fin de la fonction en cours.

Un truc auquel je n’ai rien compris pour le moment : les function closures. Je ne suis pas sûr d’avoir bien saisi l’utilité du bidule, surtout que ça rend compliquée la lecture du code. J’ai bien une solution pour l’exercice Fibonacci du Go Tour, mais je ne suis pas convaincu.

Pointeurs à la noix

&variable est l’adresse du contenu de la variable

*pointeur est le contenu situé à l’adresse « pointeur »

Par contre il me semble qu’on ne puisse rien faire d’autre (pas d’arithmétique comme en C). A voir ce que ça donne en gestion de la mémoire…

Structures

type Vertex struct {
	X int
	Y int
}

func main() {
	fmt.Println(Vertex{1, 2})
	fmt.Println(Vertex{Y: 1, X: 2})
}

Du classique. En Go, le type des variables est situé après le nom de la variable. On peut déréférencer par un raccourci. Dans l’exemple précédent :

v := Vertex{1, 2}
p := &v
p.X = 1e9
fmt.Println(v)

On peut écrire p.X à la place de (*p).X. On aurait pu mettre un -> comme en PHP…

Un raccourci piège

Ce raccourci, qui marche aussi pour (&p).X en p.X, est pratique pour coder vite, mais je trouve qu’il ne devrait pas être autorisé, vu le peu que ça fait gagner et la confusion qui peut s’ensuivre avec ces foutus pointeurs.

Tableaux

Du côté des tableaux, même tendance :

primes := [6]int{2, 3, 5, 7, 11, 13}
fmt.Println(primes)

Slices

Utilisation courante, comme en Rust ou en Python, avec la syntaxe suivante :

a[low : high]

low est la borne inférieure de la sélection, incluse dans le résultat, high est la borne supérieure exclue de la sélection. Les slices ont une longueur len() et une capacité cap().

Attention : []int{...} est de type slice, [10]int{...} est de type tableau ! Ces deux types sont très différents, car les tableaux sont statiques (leur longueur est définie et fixe). Pour passer un tableau en paramètre d’une fonction, il vaut souvent mieux le transformer en slice par tableau[:].

Pour créer un tableau de longueur variable, il faut utiliser :

a := make([]int, len, cap)  // cap est optionnel

Pour parcourir un tableau dans une boucle for :

func main() {
	daysOfWeek := [7]string{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}

	for index, value := range daysOfWeek {
		fmt.Printf("Day %d of week = %s\n", index, value)
	}
}

Les listes de type (valeur, clé) sont des maps en Go. Exemple :

var m = map[string]Vertex{
	"Bell Labs": Vertex{
		40.68433, -74.39967,
	},
	"Google": Vertex{
		37.42202, -122.08408,
	},
}

Il vaut mieux tester l’existence de la clé, pour éviter les soucis avec certains types de valeurs comme les entiers. Exemple :

func main() {
	m := make(map[string]int)

	m["Answer"] = 42
	fmt.Println("The value:", m["Answer"])

	delete(m, "Answer")
	fmt.Println("The value:", m["Answer"]) // Retournera 0 !!

	v, ok := m["Answer"]
	fmt.Println("The value:", v, "Present?", ok) // Retournera 0, 'false'
}

Contrôles

Le while classique se réduit à un for.

sum := 1
for sum < 1000 {
	sum += sum
}

Les boucles if et for n'ont pas besoin de parenthèse mais le code en condition/répétition doit être entouré d'accolades.

Le if peut également contenir une pré-condition à exécuter, par exemple :

func pow(x, n, lim float64) float64 {
	if v := math.Pow(x, n); v < lim {
		return v
	}
	return lim
}

Idem pour le switch qui peut aussi servir élégamment pour des conditions multiples :

func main() {
	t := time.Now()
	switch {
	case t.Hour() < 12:
		fmt.Println("Good morning!")
	case t.Hour() < 17:
		fmt.Println("Good afternoon.")
	default:
		fmt.Println("Good evening.")
	}
}

Orientation objet

Y a pas. Cherchez pas, Go n’est pas un langage objet. C’est dommage pour une certaine organisation du code qui est plus simple avec un langage objet, mais tout ce qui est fait en langage objet peut être fait en langage classique. Faut juste avoir une organisation de développement plus rigoureuse.

Côté performances, c’est évidemment beaucoup mieux en non-objet.

Méthodes

On peut simuler les méthodes en utilisant des fonctions.

Exemple :

func (v Vertex) Abs() float64 {
	…
}

Sera ensuite appelée via v.Abs() (v étant une structure de type Vertex). On aurait aussi pu faire :

func Abs(v Vertex) float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

La limite naturelle est celle du package : on ne peut pas déclarer une méthode sur une structure définie dans un autre package. Ça risquerait de brouiller l’écoute.

Si on souhaite modifier l’objet appelant, il faudra utiliser un pointeur sur la structure. Sinon on sera limité à la lecture des différentes valeurs.

Avec la fameuse structure Vertex, cela donnerait :

func (v Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y) // On ne fait que lire et retourner un résultat
}

func (v *Vertex) Scale(f float64) {
	v.X = v.X * f // Ici on modifie la valeur de « l’objet » appelant
	v.Y = v.Y * f
}

Interfaces

Voir aussi l’anecdote sur Rust et sur l'inventeur de Java. Après, comme Go n'est pas un langage objet, cette notion d'interface me semble moins utile, sauf pour vérifier qu'on a bien construit une méthode "commune" à plusieurs structures, ou pour simuler une sorte de polymorphisme.

Lorsqu'on implémente l'interface sur une structure, il n'y a pas de rappel (déclaratif) à cette interface, du genre implements en Java : il suffit d'écrire la fonction ayant le nom de la fonction déclarée dans l'interface.

On peut faire pas mal de manipulations avec les interfaces, comme des interfaces vides (interface{}) qui permet de gérer des types inconnus, ce qui est le cas de la fonction fmt.Print par exemple.

On peut aussi adresser une interface par son type, voir le tester !

t := i.(T)
t, ok := i.(T) // ici, on teste en plus l'existence de l'interface 
               // pour le type (structure) donné
...
func do(i interface{}) {
	switch v := i.(type) {
	case int:
		fmt.Printf("Twice %v is %v\n", v, v*2)
	case string:
...

Concurrence et threads

Un gros morceau de Go me semble être la gestion des threads et des appels concurrentiels.

Thread simple

On lance go <fonction> est le tour est joué : l'exécution est lancée en //, mais en partageant l'espace mémoire. Ce mode d'exécution permet juste la parallélisation, lorsqu'on attend des résultats de différents appels externes, par exemple.

Canaux ("channels")

Là on rentre dans la séparation de la mémoire.

ch <- v    // Send v to channel ch.
v := <-ch  // Receive from ch, and
           // assign value to v.

Comme les maps et les slices, une initialisation est nécessaire via make(chan type). On peut, en option, indiquer le nombre de lancements concurrents possibles pour le canal (par exemple make(chan int, 5)). Si on dépasse, on a un arrêt du programme avec un message du genre :

fatal error: all goroutines are asleep - deadlock!

Le canal peut être ensuite utilisé comme paramètre dans une fonction, où on y renvoie un contenu via l'opérateur <-. Pour recevoir le résultat, une fois que toutes les opérations du canal sont terminées, on assigne res := <- c (où c est un canal).

Autres éléments

Il existe encore d'autres éléments pour la gestion de la concurrence, mais je regarderai cela quand j'en aurai vraiment besoin.