SDR

Introduction à Go

Olivier Lemer

Sommaire

Comparaison avec Java

Ressources et Développement

Bases du langage

Héritage et Composition

Concurrence

Programmation TCP-IP

1.

2.

3.

4.

5.

6.

Comparaison avec Java

Comparaison avec Java

Histoire

2007 - Créé en interne pour Google, par Robert Gresemer, Rob Pike et Ken Thompson.

Pensé pour

Logiciels serveurs

2009 - Devient Open Source avec communauté très active.

2012 - Publication de la version 1.0, stable.

Scalability (10^6 machines)

(Concurrence builtin au langage)

Comparaison avec Java

Similarités

  • Typage statique fort
  • Garbage Collector
  • Memory safety partielle (nulls, out-of-bounds)
  • Variables toujours initialisées avec leur "zéro"
  • Méthodes
  • Interfaces
  • Assertion sur les types (instanceof)
  • Introspection (reflection)
  • Closures et captures

Différences

  • Pas de VM (compilation vers un executable)
  • Librairies incluses dans executable (static linking)
  • Fonctions as first class citizens
  • Strings, Maps, Arrays, Slices sont builtin
  • Concurrence est builtin
  • Pas de classes
    • Pas de constructeurs
    • Pas d'héritage (mais : composition)
  • Pas d'implémentation par défaut des méthodes d'une interface
  • Pas d'exceptions (sauf pour abort)
  • Pas d'annotations (mais tags dans structs)

Philosophie de Go : lisibilité et de simplicité, afin d'éviter les erreurs d'implémentations.

Comparaison avec Java

Hello, World!

public class Main {
    public static void main(String[] args) {
    System.out.println("Hello, world!"); }
}
main.java
package main

import "fmt"

func main() {
    fmt.Println("Hello, World!")
}
hello.go
public class Main {
    public static void main(String[] args) {
    	System.out.println("Hello, world!");
    }
}

Point virgule optionnel

Ressources et développement

Sur le site officiel : golang.org

  • Édition et exécution de code en ligne
  • Tutoriels et exercices de prise en main (Documents > Learning Go > A tour of Go)
  • Installation compilateur, librairies et utilitaires (Documents > Installing Go)
  • Organisation d'un projet (Documents / Learning Go / How to write Go code)
  • Outils de développement (Documents / Learning Go / Editor plugins and IDEs)
  • Bonnes pratiques (Documents / Learning Go / Effective Go)
  • Et autres références, articles, vidéos ...

Ressources et Développement

Ressources et Développement

Structure d'un projet

.
├── server
│   ├── server.go
│   └── db.go
├── main.go
├── util.go
└── go.mod

Go module

  • Contient le fichier go.mod
  • Regroupe plusieurs packages en un projet.
  • Optionel s'il n'y a qu'un package.

Go packages

  • Chaque dossier constitue un package du même nom.
  • Tous fichiers d'un même package partagent tout sans besoin d'import explicite.
  • Chaque fichier commence par package <name> pour indiquer son package.
  • Le nom des fichiers est sans importance.

Main package

Le package à la racine du module est spécial et doit contenir une fonction main().

Export entre packages

Variable/Fonction avec Majuscule : identifiant exporté.

Variable/Fonction avec minuscule : identifiant pas exporté.

Ressources et Développement

Structure d'un projet

Notez : pas besoin d'importer util.go dans hello.go, car ils sont dans le même package.

package main

import "fmt"

func main() {
    fmt.Println(reverse("Hello, World!"))
}
/hello.go
package main

func reverse(s string) string {
    b := []rune(s)
    for i := 0; i < len(b)/2; i++ {
    	j := len(b) - i - 1
        b[i], b[j] = b[j], b[i]
    }
    return string(b)
}
/util.go

Ressources et Développement

Structure d'un projet

package main

import (
    "fmt"
    "module_name/util"
)

func main() {
    fmt.Println(util.Reverse("Hello, World!"))
}
/hello.go
package util

func Reverse(s string) string {
    b := []rune(s)
    for i := 0; i < len(b)/2; i++ {
    	j := len(b) - i - 1
        b[i], b[j] = b[j], b[i]
    }
    return string(b)
}
/util/util.go

Même exemple avec plusieurs packages.

Reverse avec majuscule => exporté

Package util importé

Ressources et Développement

Structure conventionnelle d'un projet

.
├── cmd
│   ├── server
│   │   └── main.go
│   └── client
│       └── main.go
├── internal
│   ├── server
│   │   └── server.go
│   ├── client
│   │   ├── cmd_parser.go
│   │   └── client.go
│   └── mutex
│       └── mutex.go
└── go.mod

Packages cmd

Contient uniquement les sources d'exécutables, qui référencent la logique interne.

Généralement très légers.

Packages internes

Modularisent la logique interne.

Logique du serveur.

Logique du client.

Logique commune.

Ressources et Développement

Import de packages externes

go get [url]

Obtient le package pointé par l'url, et crée une dépendance à celui-ci dans go.mod.

go get github.com/example

Le package peut ensuite être importé et utilisé dans tout fichier du module.

import "github.com/example"

Nous ne le ferons pas dans les labos de ce cours.

go build [package]

Compile un package (ou un fichier formant un package) en un executable dans le dossier courant.

Ressources et Développement

Compiler et installer

go build hello.go

go install [package]

Compile un package (ou un fichier formant un package), puis installe l'executable dans $GOBIN, donc $HOME/go/bin/ par défaut.

go install

go run [package]

Compile et exécute un package (ou un fichier formant un package) d'éxécutable.

go run hello.go

Ressources et Développement

Tester

go test

Compile et execute toute fonction Test* dans tout fichier *_test.go.

go test
pakage util

import "testing"

func TestSomething(t *testing.T) {
    // ...
}
util/util_test.go

Bases du langage

Collections

Bases du langage

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

Types de base

Bases du langage

Variables

var <name> <type> = <value>

Type optionel s'il peut être inféré de la valeur assignée.

Valeur peut être omise : valeur "zéro" alors utilisée.

<name> := <value>

Forme équivalente succinte (uniquement pour variables locales muables)

Symbole ':=' utilisé à la place de '='.

Type omis car inféré de la valeur assignée.

Peut être const pour créer une constante.

Bases du langage

Tableaux et slices

Tableau [5]byte

Slice du tableau

Pointeur sur début

Taille

Capacité

'o'

'l'

'l'

'e'

'h'

[n]type // Type d'un array de taille n et de type type

Syntaxe du type d'un tableau

[2]string{"Hello", "there"}

Syntaxe d'un littéral tableau

tableau[a:b] // slice entre a (incl) et b (excl) de tableau

Syntaxe d'un slice

2

4

tableau := [5]byte{'h','e','l','l','o'}

slice := tableau[1:3] // ['e', 'l']

slice = append(slice, 'i') // ['e', 'l', 'i']

Augmente la taille et insère la valeur.

Si le tableau est trop petit, en crée un nouveau et pointe sur celui-là.

Bases du langage

Tableaux et slices

Tableau [5]byte

Slice du tableau

Pointeur sur début

Taille

Capacité

'o'

'i'

'l'

'e'

'h'

tableau := [5]byte{'h','e','l','l','o'}

slice := tableau[1:3] // ['e', 'l']

slice = append(slice, 'i') // ['e', 'l', 'i']
[n]type // Type d'un array de taille n et de type type

Syntaxe du type d'un tableau

[2]string{"Hello", "there"}

Syntaxe d'un littéral tableau

tableau[a:b] // slice entre a (incl) et b (excl) de tableau

Syntaxe d'un slice

3

4

Note : modifier la slice modifie le tableau sous-jacent.

Augmente la taille et insère la valeur.

Si le tableau est trop petit, en crée un nouveau et pointe sur celui-là.

Bases du langage

Tableaux et slices - Question

tableau := [5]byte{'h','e','l','l','o'}

slice := tableau[0:3] // ['h', 'e', 'l']

slice[0] = 'R'

slice = append(slice, 'i')
slice = append(slice, 'u')
slice = append(slice, 'm')

slice[0] = 'H'

Que contient tableau ?

  1. Heliu
  2. Helium
  3. heliu
  4. helium
  5. Reliu
  6. Relium

Initialement, la slice pointe sur le tableau. Tout changement sur la slice s'applique donc au tableau, y inclus les append.

Lorsqu'on tente d'append au delà de la capacité du tableau ('m'), un nouveau tableau est créé et la slice pointe dessus.

À partir de ce moment, tout changement se fait sur ce nouveau tableau, 'R' n'est donc pas remplacé par 'H' ligne 11.

Bases du langage

Tableaux et slices

[n]type // Type d'un array de taille n et de type type

Syntaxe du type d'un tableau

[2]string{"Hello", "there"}

Syntaxe d'un littéral tableau

tableau[a:b] // slice entre a (incl) et b (excl) de tableau

Syntaxe d'un slice

make([]type, len, cap) // slice de type type, longeur len et capacité cap

Création d'un slice avec make

Tableau [5]byte

Slice du tableau

Pointeur sur début

Taille

Capacité

\0

3

5

slice := make([]byte, 3, 5)

\0

\0

\0

\0

Crée un tableau de zéros de taille 5,

et crée un slice de capacité 3 dessus.

Bases du langage

Maps

make(map[int]string) // map vide de int vers string

Création d'une map avec make

Syntaxe d'un littéral map

map[int]string{1: "un", 2: "deux"}
map[int]string // Type d'une map de clés int et valeurs string

Syntaxe du type d'une map

nombres := make(map[int]string)

nombres[1] = "un"  // Ajout d'une valeur
nombres[1] = "one" // Remplacement d'une valeur

un := nombres[1]   // Lecture d'une valeur

delete(nombres, 1) // Suppression d'une valeur

un, ok := nombres[1] // Lecture et vérification d'une valeur

Modification d'une map

true ssi la valeur existe dans la map.

Bases du langage

Sets

noms := make(map[string]bool)
mots := make(map[string]struct{})

Map largement suffisant : la valeur peut être une structure vide.

N'existe pas.

Bases du langage

Conditions et boucles

Bases du langage

Instructions conditionnelles

if <init>; <cond> {
    // ...
} else if {
    // ...
} else {
    // ...
}

Statement court (optionnel)

Comme pour for, permet d'initialiser une variable qui existera dans le corps du if et du else.

Pas de parenthèses nécessaires  pour la condition.

Blocs else et else if optionnels

Bases du langage

Instructions conditionnelles

switch <init>; <value> {
case <value> :
    // ...
    fallthrough
case <value> :
    // ...
default :
    // ...
}

break par défaut.

Pour forcer l'évaluation du case suivant, utiliser fallthrough

Bloc default optionnel.

Switch sur une valeur

switch {
case <cond> :
    // ...
case <cond> :
    // ...
default :
    // ...
}

Si la valeur est omise, alors les case prennent des conditions.

On obtient alors un équivalent exact de if-elseif-elseif-else

Switch sur rien

Statement court (optionnel)

Comme pour le if.

Bases du langage

Boucles

for <init>; <condition>; <post> {
    // ...
}

<init> et <post> sont optionnels

for ; <condition>; {
for <condition> {

Lorsque les deux sont omis, les ';' peuvent l'être aussi.

Boucle for

for i, v := range <iterable> {
    // ...
}

<condition> est optionnel.

for { // infinite loop

Boucle foreach

Mot-clé range fait partie de la syntaxe.

Peut être un array ou une map

Donne l'index (ou la clé) et la valeur. Utiliser le symbole '_' pour ne pas récupérer dans une variable.

Bases du langage

Boucles

Boucle while

N'existe pas.

for <condition> {

Boucle for complètement équivalente

Bases du langage

Fonctions

Bases du langage

Fonctions

func compare[T comparable](a T, b T) bool {
<T> <constraints>

Paramètres de type

Paramètres

Type de retour

(Fonctions génériques)

  • Optionnel
  • Liste de paramètres de forme
  • <T> le nom du paramètre
  • <constraints> décrit des contraintes sur le type
  • Liste de paramètres de forme
<name> <type>
  • Le type peut être omis s'il est celui du paramètre suivant :
func add(x, y int) int {
  • Optionnel si la fonction ne retourne rien (pas de void)
  • Pour retourner plusieurs valeurs
func origin() (int, int) {
  • Valeurs retournées peuvent être nommées
func origin() (x int, y int) {
    x = 1;
    y = 2;
    return
}

Elles seront alors des variables dans la fonction, et seront retournées automatiquement.

  • Si deux partagent leur type, le premier peut l'omettre
func origin() (x, y int) {

Bases du langage

Fonctions - exemples

func add(x, y int) int { // type de x omis car identique à y
    return x + y
}
func swap(x, y string) (string, string) { // Plusieurs valeurs retournées
    return y, x
}
func split(sum int) (x, y int) { // Valeurs retournées nommées.
    x = sum * 4 / 9              // Variables x et y déjà existentes.
    y = sum - x
    return                       // Retourne x et y automatiquement.
}
func swap(x, y string) (string, string) { // Plusieurs valeurs retournées
    return y, x
}
a, b := swap("Hello", "world!"); // Attribution à deux variables
_, _ := swap("Hello", "world!"); // Possibilité d'ignorer. Nombre doit correspondre.

Bases du langage

Fonctions comme first class citizens

func(<parameter_types>) <return_types>

Syntaxe du type d'une fonction

func(<parameters>) <return_types> { <body> }

Syntaxe d'une fonction anonyme

func square(n int) int {return n * n}

func main() {
    var f func(int) int                       // Variable de type func(int) int

    f = func(n int) int { return -n }         // Attribution avec fonction anonyme

    f = square                                // Attribution avec fonction existente
}

func applyTwice(f func(int) int, n int) int { // Fonction comme argument
    return f(f(n))
}

Exemple

Bases du langage

Fonctions deviennent closures

Définition : objet représentant une fonction et l'environnement dans lequel elle a été créée.

func getUidGenerator() func() int {
    var nextUid int = 0
    return func() int {
        nextUid += 1
        return nextUid
    }
    
}

func main() {
    genUid := getUidGenerator()
    id1 := genUid() // 1
    id2 := genUid() // 2
}

genUid

function

nextUid += 1
return nextUid;

Environment

nextUid: 0

nextUid utilisée dans la fonction, elle est capturée par la closure.

nextUid: 1

Bases du langage

Fonctions deviennent closures

Définition : objet représentant une fonction et l'environnement dans lequel elle a été créée.

func getUidGenerator() func() int {
    var nextUid int = 0
    f := func() int {
        nextUid += 1
        return nextUid
    }
    nextUid = 42
    return f
}

func main() {
    genUid := getUidGenerator()
    id1 := genUid() // 43
    id2 := genUid() // 44
}

genUid

function

nextUid += 1
return nextUid;

Environment

nextUid: 42

Si une variable capturée change, elle change dans la closure aussi.

Bases du langage

Structures et composition

Bases du langage

Déclaration de types

type intGenerator func() int

type image [][]uint32

type celsius float64
type farenheit float64

var ebulitionC celsius = 100
var ebuilitonF farenheit = ebulitionC // ERROR : Cannot assign celsius to farenheit

Exemples

type <new_type> <existing_type>

Déclarer un nouveau type

type <type_alias> = <existing_type>

Déclarer un alias pour un type donné

type celsius = float64
type farenheit = float64

var ebulitionC celsius = 100
var ebuilitonF farenheit = ebulitionC // Correct ; même type : float64

Exemples

Bases du langage

Structures

struct {
    <name1> <type1>
    <name2> <type2>
    // ...
}

Définition : une structure est simplement un ensemble de champs nommés.

Syntaxe du type d'une structure

type Person struct {
    name string
    age  int
}

var fred Person

Généralement associé à un nouveau type immédiatement

var fred struct {
    name string
    age  int
};

Mais reste un type comme un autre, peut être utilisé comme tel

Bases du langage

Structures

struct {
    <name1> <type1>
    <name2> <type2>
    // ...
}

Définition : une structure est simplement un ensemble de champs nommés.

Syntaxe du type d'une structure

Note: les règles d'export s'appliquent aux champs : pour qu'un champ soit public en dehors du package, il doit commencer par une majuscule.

// Création d'une struct avec champs "zéros"
var fred Person // {"", 0}

// Création avec valeurs initiales fournies
var fred = Person{"fred", 99} // {"fred", 99}

// Création avec valeurs initiales fournies et nommées, ordre arbitraire
var fred = Person{age: 99, name: "fred"} // {"fred", 99}

// Création avec valeurs initiales partiellement fournies, "zéros" ailleurs
var fred = Person{name: "fred"} // {"fred", 0}

Syntaxe de création d'une valeur de type struct

Bases du langage

Méthodes

func (<name> <type>) <name> (<parametres>) <return_types> { // ...

Syntaxe d'une méthode : déclaration du récepteur de la fonction avant son nom

  
  
func (p Person) Majeur() bool {
    return p.age >= 18
}

if fred.Majeur() { // ...

Exemple

Peut donc être pensé comme du syntactic sugar pour

Note : le récepteur n'est qu'un paramètre comme un autre.

func Majeur(p Person) bool {
    return p.age >= 18
}

if Majeur(fred) { // ...

Bases du langage

Méthodes

func (<name> <type>) <name> (<parametres>) <return_types> { // ...

Syntaxe d'une méthode : déclaration du récepteur de la fonction avant son nom

  
  

<type> ne peut pas être un type builtin

func (s string) MyLength() int { // ERROR : string ne peut pas être récepteur

En revanche, <type> peut être un type équivalent à un type builtin

type MyString string

func (s MyString) MyLength() int { // Valide (!)

Bases du langage

Interfaces

interface {
    <method1>
    <method2>
    // ...
}

Syntaxe du type d'une interface

Définition : représente la catégorie des types qui implémentent des méthodes données.

type Walker interface {
    StepsPerSecond() int
}

type Person struct {
    // ...
}

func (p Person) StepsPerSecond() int { // Person est désormais un Walker
	// ...
}

Important

Les interfaces sont satisfaites implicitement, et non explicitement comme en Java.

Si une struct implémente les méthodes d'une interface, alors elle satisfait cette interface.

Bases du langage

Interfaces

type Walker interface {
    StepsPerSecond() int
}

type Person struct { // ...

func (p Person) StepsPerSecond() int { // Person est désormais un Walker
	// ...
}

func main() {
    var p Person
    
    var w Walker = p        // Polymorphisme
}

Polymorphisme

var v <interface> = <valeur> // si <valeur> satisfait <interface>

Exemple

Bases du langage

Composition vs. héritage

class Runner {
    // ...

    public void run(Task task) {
        task.run();
    }
    
    public void runAll(Task[] tasks) {
        for (Task task : tasks) {
            run(task);
        }
    }
}
CountingRunner runner = new CountingRunner();

Task[] tasks = // ...

runner.runAll(tasks);

Comptera chaque tache deux fois, à cause de dynamic dispatch.

class CountingRunner extends Runner {
    private int count;

    // ...
    
    @Override public void run(Task task) {
        count++;
        super.run(task);
    }
    
    @Override public void runAll(Task[] task) {
        count += tasks.length;
        super.runAll(tasks);
    }
}

Dépend de l'implémentation de Runner : si runAll appelait task.run(), pas de problème.

Risques de bugs étranges, dus au fort couplage.

: héritage

Bases du langage

Composition vs. héritage

type Runner struct{}

func (r Runner) Run(t Task) {
    t.Run()
}

func (r Runner) RunAll(ts []Task) {
    for _, t := range ts {
        r.Run(t)
    }
}
CountingRunner runner = new CountingRunner();

Task[] tasks = // ...

runner.runAll(tasks);
type CountingRunner struct {
    runner Runner
    count  int
}

func (r CountingRunner) Run(t Task) {
    r.count++
    r.runner.Run(t)
}

func (r CountingRunner) RunAll(ts []Task) {
    r.count += len(ts)
    r.runner.RunAll(ts)
}

: composition

Comptera chaque tache une seule fois : plus de dynamic dispatch.

Couplage faible, simplicité accrue, moins de bugs.

Bases du langage

Composition vs. héritage : héritage

Runner

run(Task t)

runall(Task[] ts)

CountingRunner

run(Task t)

runall(Task[] ts)

hérite de

appelle

appelle

Dynamic Dispatch

Bases du langage

Composition vs. héritage : composition

Runner

run(Task t)

runall(Task[] ts)

CountingRunner

run(Task t)

runall(Task[] ts)

est composé de

appelle

appelle

  • Simplification des dépendances : Runner est indépendant de CountingRunner
  • Runner peut être créé après CountingRunner.
  • Runner peut être remplacé facilement par un mock pour les tests.

Mais, on doit réécrire ce qu'on aimerait simplement hériter.

Or do we...

Bases du langage

Struct embedding

type Employee struct {
    Person

    employeeId int
}

Si un champ d'une struct n'est qu'un type, on dit qu'il est incorporé à cette struct.

  • Similaire à de l'héritage, mais l'incorporé reste indépendant de l'incorporant.
  • Un champ est implicitement créé avec comme nom le type incorporé
var e Employee
e.Person.Name // Champ de Person
  • Tous les champs et méthodes du type incorporé sont accessibles directement
var e Employee
e.Name      // Champ de Person
e.GetName() // Méthode de Person

Bases du langage

Struct embedding

type Runner struct {
    name string
}

func (r Runner) GetName() string { return r.name }

// ...
type CountingRunner struct {
    runner Runner
    count int
}

// Obligé de réécrire GetName qui ne fait qu'appeler son parent
func (r CountingRunner) GetName() string { return r.runner.Name() }

// ...

Exemple - sans struct embedding

Bases du langage

Struct embedding

type Runner struct {
    name string
}

func (r Runner) GetName() string { return r.name }

// ...
type CountingRunner struct {
    Runner
    count int
}

// Plus nécessaire : automatiquement défini sur CountingRunner
// func (r CountingRunner) GetName() string { return r.runner.Name() }

// ...

Exemple - sans struct embedding

Bases du langage

Struct embedding

type Person struct {
    Name string
}

func (p Person) Introduce() {
    fmt.Println("Hi, I'm ", p.Name)
}


type Employee struct {
    Person
    EmployeeId int
}

func (e Employee) GetEmail() string {
    return e.Name + "@entreprise.ch"
}


func main() {
    e := Employee{Person: Person{"Peter"}, EmployeeId: 123}
    
    e.Introduce() // Fonctionne, et affiche "Hi, I'm Peter"
}

Autre exemple

Bases du langage

Conversion de types

Bases du langage

Conversion de types

<new_type>(<value>)

Syntaxe d'une conversion de type (cast) statique

var x float64 = 3.6
var n int = 2

n = int(x)     // Avec troncation => 2
x = float64(n) // => 2.0

Exemples

s := "aéb"      // string, utf8 par défaut, 32 bits car 'é' prend 2 bytes
b := []byte(s)  // Conversion vers slice de bytes : longueur 4
s2 := string(b) // Conversion vers string à nouveau
r := []rune(s)  // Conversion vers slice d'unicode : longueur 3
  • Pas de conversion implicite en Go.
  • Une conversion de type statique n'échouera jamais pendant exécution.

Bases du langage

Conversion de types

<value>.(<type>)

Syntaxe d'une conversion de type (cast) dynamique, ou "interface conversion".

var w Walker = Person{}

var p = w.(Person) // Panic si w pas Person

Version risquée

var w Walker = Person{}

var p, ok = w.(Person)

if ok {
    // Conversion réussie
} else {
    // Conversion échouée
}

Version safe

Bases du langage

Switch sur type

switch w.(type) {
case Person:
    // ...
case Walker:
    // ...
}

Syntax de conversion dynamique réutilisée pour switch sur type

Bases du langage

Pointeurs

Bases du langage

Pointeurs

*<type> // Pointeur sur une valeur de type <type>

Syntaxe du type d'un pointeur

new(<type>) // Allocation de mémoire de type <type> et création d'un pointeur dessus.

Création d'un pointeur sur variable anonyme

&<variable> // Pointeur sur <variable>

Obtenir un pointeur sur une valeur

*<pointeur> // Valeur pointée par <pointeur>

Déréférencer un pointeur

p := new(int)   // Pointeur sur int, type *int
fmt.Println(*p) // Déréférencement du pointeur

func newIntPointer() *int {
    var dummy int
    return &dummy
}
p := newIntPointer() // Pointeur sur int, type *int
// Pas de dangling pointer : Go sait qu'il doit garder dummy en mémoire.

Exemples

Bases du langage

Pointeurs sur struct

func (p *Person) IncAge() {
    p.age++
}

Le récepteur d'une méthode est un paramètre comme un autre : peut être un pointeur

Le récepteur peut alors être nul ! Parfois utile.

func (p *Person) Name() string {
    if p == nil {
        return "anonymous"
    }
    return p.name
}

Accéder aux champs d'une struct pointée se fait normalement (pas de '->' comme C)

p := new(Person)
p.name = "Johny"
jeff := Person{}
jeff.IncAge() // Valide, même si jeff n'est pas un pointeur

Le récepteur est implicitement référencé si besoin.

Concurence

Concurrence

"Do not communicate by sharing memory;

instead, share memory by communicating."

- Rob Pike

Concurrence

Goroutines

Définition : fonctionnellement comme un thread, mais plus léger :

démarre avec peu de mémoire et alloue plus si besoin,

peut exister par centaines de milliers,

builtin à Go.

Syntaxe de lancement d'une goroutine

go <statement> // Execute <statement> dans une goroutine
go fetchDocument(documentName)

Exemple

Techniquement :

Go réparti les goroutines sur les threads de l'OS.

Une goroutine bloquée ne bloque pas de thread.

Attention : si main termine, toutes les goroutines sont tuées.

Concurrence

Channels

Définition : Canal de communication concurrente avec buffer statique.

Syntaxe de création d'un channel

make(chan <type>, <cap>) // Crée un channel de type <type>, avec un buffer de taille <cap>

Syntaxe d'envoi de données sur un channel

<channel> <- <value> // Envoi de <value> de type correspondant sur le channel c

Syntaxe de réception de données d'un channel

<-<channel> // Réception d'une valeur depuis le channel <channel>

Optionnel : si omis, buffer de taille nulle.

Exemple

c := make(chan string, 1)
c <- "Hello, receiver!"
s := <-c
fmt.Println(s) // Hello, receiver!

Bloquant si le buffer est plein (ou de taille nulle si aucune goroutine en attente de lecture)

Bloquant si le buffer est vide (ou de taille nulle si aucune goroutine en attente d'écriture)

Concurrence

Channels

Définition : Canal de communication concurrente avec buffer statique.

Syntaxe de création d'un channel

make(chan <type>, <cap>) // Crée un channel de type <type>, avec un buffer de taille <cap>

Syntaxe d'envoi de données sur un channel

<channel> <- <value> // Envoi de <value> de type correspondant sur le channel c

Syntaxe de réception de données d'un channel

<-<channel> // Réception d'une valeur depuis le channel <channel>

Optionnel : si omis, buffer de taille nulle.

Restrictions sur un channel

chan<- string

Type d'un write-only channel

<-chan string

Type d'un read-only channel

Bloquant si le buffer est plein (ou de taille nulle si aucune goroutine en attente de lecture)

Bloquant si le buffer est vide (ou de taille nulle si aucune goroutine en attente d'écriture)

Concurrence

Channels

Goroutine 1

Goroutine 1

Concurrence

Channels - fermeture

Syntaxe de fermeture d'un channel

close(<channel>)

Syntaxe de réception de fermeture

v, ok := <-c

Seul un envoyeur est autorisé à fermer un channel.

Un channel n'est pas comme un fichier :

Pas besoin de le fermer, sauf pour avertir le récepteur qu'il n'y aura pas d'autre message.

false ssi le channel a été fermé par un envoyeur.

Concurrence

Channels - range

for <msg> := range <channel> {
    // do stuff
}

Permet de lire un channel jusqu'à ce qu'il soit fermé.

for {
    <msg>, ok := <channel>
    if ok {
        // do stuff
    } else {
        break
    }
}

Concurrence

Channels - Select

Syntaxe du select

select {
case msg := <-chan1:
    // chan1 had a message, do something with it
case msg := <-chan2:
    // chan2 had a message, do something with it
default:
    // neither had a message
}

default est optionnel : si omis, le select sera bloquant.

Permet de

  • attendre plusieurs channels, et
  • optionnellement, les rendre non-bloquants

Note : Seule une branche est exécutée, même si plusieurs channels ont un message.

On met donc souvent le select dans une boucle.

Concurrence

Defer

defer <statement>

Garantie que la connection sera fermée en fin de programme.

Permet de repousser l'exécution d'un statement à la fin de la fonction.

func main() {
    conn, err := net.Dial("udp", srvAddr)
    
    defer conn.Close()
    
    // ...
}

Exemple

Concurrence

Exemple

Supposons l'existence de makeQuery(string) Result

func makeQueriesSequential(queries []string) (results []Result) {
    for _, q := range queries {
        results = append(results, makeQuery(q))
    }
    return
}

Créons makeQueries qui gère plusieurs requêtes.

Version simple, séquentielle

Concurrence

Exemple

Supposons l'existence de makeQuery(string) Result

func makeQueriesParallel(queries []string) (results []Result) {
    c := make(chan Result)
    for _, q := range queries {
        go func() { c <- makeQuery(q) }()
    }
    
    for _, _ := range queries {
        result := <-c
        results = append(results, result)
    }
    return
}

Créons makeQueries qui gère plusieurs requêtes.

Version parallèle

Concurrence

Exemple

Supposons l'existence de makeQuery(string) Result

func makeQueriesTimeout(queries [string]) (results []Result) {
    c := make(chan Result, len(queries))
    for _, q := range queries {
        go func() { c <- makeQuery(q) }
    }
    
    timeout := time.After(80 * time.Millisecond)
    for _, _ := range queries {
        select {
        case result := <-c:
            results = append(results, result)
        case <-timeout:
            fmt.Println("Timed out")
            return
        }
    }
    return
}

Créons makeQueries qui gère plusieurs requêtes.

Version parallèle++

Note : on donne un buffer non-vide, pour que les goroutines n'attendent pas éternellement en cas de timeout.

Concurrence

Exemple 2

var nextId = make(chan int)

func handler(w http.ResponseWriter, q *http.Request) {
    fmt.Fprintf(w, "<h1>You have id %v<h1>", <-nextId)
}

func main() {
    http.HandleFunc("/next", handler)
    go func() {
        for i := 0; ; i++ {
            nextId <- i
        }
    }()
    http.ListenAndServe("localhost:8080", nil)
}

Pas besoin de compteur partagé, ni de lock, le channel fait tout pour nous !

Web page counting its users

Concurrence

Bonnes pratiques

Aucun état ne doit être partagé entre goroutines.

Gestion d'état dynamique

Un état n'existe donc généralement qu'au sein d'une même goroutine.

type User struct {
    name string
    age  int
}

func handleUsers(newUsers <-chan User, leavingUsers <-chan User) {
	users := make(map[string]User)
    for {
    	select {
        case u := <-newUsers:
        	users[u.name] = u
        case u := <-leavingUsers:
            delete(users, u.name)
        }
    }
}

func main () {
	go handleUsers(/* ... */)
}

La gestion de cet état est sa responsabilité.

Concurrence

Bonnes pratiques

Pour extraire une information de cet état, on peut fournir un channel de retour.

Obtention d'état dynamique

type getUserReq struct {
    name   string
    result chan<- *User
}

func handleUsers(/* ... */, getUser <-chan getUserReq) {
	users := make(map[string]User)
    for {
    	select {
        /* ... */
        cas req := <-getUser:
            if u, ok := users[req.name]; ok {
                req.result <- &u
            } else {
            	req.result <- nil
            }
        }
    }
}

Concurrence

Bonnes pratiques

Il ne faut qu'un récepteur par channel. (Sinon, lequel lira en premier sera imprévisible)

Communication

Par contre, plusieurs goroutines peuvent écrire en parallèle sur un même channel.

TCP-IP

Programmation TCP-IP en Go

const srvAddr = "127.0.0.1:6000"

func main() {
    conn, err := net.Dial("udp", srvAddr)
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()
    
    // Link stdin and stdout to the connection
    go forward(os.Stdout, conn)
    forward(conn, os.Stdin)
}

// Links output of src to input of dst
func forward(dst io.Writer, src io.Reader) {
    if _, err := io.Copy(dst, src); err != nil {
        log.Fatal(err)
    }
}

Client UDP

Programmation TCP-IP en Go

const srvAddr = "127.0.0.1:6000"

func main() {
    // Start listening on srvAddr
    addr, _ := net.ResolveUDPAddr("udp", srvAddr)
    conn, err := net.ListenUDP("udp", addr)
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()
    
    buf := make([]byte, 1024)
    for {
        // Get data from client
        n, cliAddr, err := conn.ReadFromUDP(buf)
        if err != nil {
            log.Fatal(err)
        }
        
        // Echo data back
        s := string(buf[0:n]) + " received from " + cliAddr.String() + "\n"
        if _, err := conn.WriteTo([]byte(s), cliAddr); err != nil {
            log.Fatal(err)
        }
    }
}

Serveur UDP

Programmation TCP-IP en Go

const srvAddr = "127.0.0.1:8000"

func main() {
    conn, err := net.Dial("tcp", srvAddr)
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()
    
    // Link stdin and stdout to the connection
    go forward(os.Stdout, conn)
    forward(conn, os.Stdin)
}

// Links output of src to input of dst
func forward(dst io.Writer, src io.Reader) {
    if _, err := io.Copy(dst, src); err != nil {
        log.Fatal(err)
    }
}

Client TCP

Programmation TCP-IP en Go

const srvAddr = "127.0.0.1:8000"

type clientOutChan chan<- string // per-client outgoing messages

var (
    joiningClients = make(chan clientOutChan) // New clients to be registered
    leavingClients = make(chan clientOutChan) // Leaving clients to be deleted
    broadcast      = make(chan string) // Messages destined to all clients
)

func main() {
    listener, err := net.Listen("tcp", srvAddr)
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()
    
    // Broadcast to all connection
    go broadcaster()
    
    // Wait for new connections
    for {
        conn, err := listener.Accept()
        if err != nil {
            log.Print(err)
        } else {
            go handleConn(conn)
        }
    }
}

func broadcaster() {
    clientOuts := make(map[clientOutChan]bool) // All connected clients
    
    for {
        select {
        case msg := <-broadcast:
            for cli := range clientOuts {
                cli <- msg
            }

        case cli := <-joiningClients:
            clientOuts[cli] = true

        case cli := <-leavingClients:
            delete(clientOuts, cli)
            close(cli)
        }
    }
}

func handleConn(conn net.Conn) {
    defer conn.Close()

    client := make(chan string)
    
    // Forward messages from client channel to connection
    go func() {
        for msg := range client {
            fmt.Fprintln(conn, msg)
        }
    }()
    
    // Send welcome message to client
    who := conn.RemoteAddr().String()
    client <- "You are " + who
    
    // Broadcasting arrival of new client
    broadcast <- who + " has arrived"

    joiningClients <- client
    
    // Broadcasting client messages
    input := bufio.NewScanner(conn)
    for input.Scan() {
        broadcast <- who + " says: " + input.Text()
    }
    
    // No more messages, client left.
    leavingClients <- client
    broadcast <- who + " has left"
}

Serveur TCP

Programmation TCP-IP en Go

const srvAddr = "127.0.0.1:8000"

type clientOutChan chan<- string // per-client outgoing messages

var (
    joiningClients = make(chan clientOutChan) // New clients to be registered
    leavingClients = make(chan clientOutChan) // Leaving clients to be deleted
    broadcast      = make(chan string) // Messages destined to all clients
)

func main() {
    listener, err := net.Listen("tcp", srvAddr)
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()
    
    // Broadcast to all connection
    go broadcaster()
    
    // Wait for new connections
    for {
        conn, err := listener.Accept()
        if err != nil {
            log.Print(err)
        } else {
            go handleConn(conn)
        }
    }
}

func broadcaster() {
    clientOuts := make(map[clientOutChan]bool) // All connected clients
    
    for {
        select {
        case cli := <-joiningClients:
            clientOuts[cli] = true

        case cli := <-leavingClients:
            delete(clientOuts, cli)
            close(cli)
            
        case msg := <-broadcast:
            for cli := range clientOuts {
                cli <- msg
            }
        }
    }
}

func handleConn(conn net.Conn) {
    defer conn.Close()

    client := make(chan string)
    
    // Forward messages from client channel to connection
    go func() {
        for msg := range client {
            fmt.Fprintln(conn, msg)
        }
    }()
    
    // Send welcome message to client
    who := conn.RemoteAddr().String()
    client <- "You are " + who
    
    // Broadcasting arrival of new client
    broadcast <- who + " has arrived"

    joiningClients <- client
    
    // Broadcasting client messages
    input := bufio.NewScanner(conn)
    for input.Scan() {
        broadcast <- who + " says: " + input.Text()
    }
    
    // No more messages, client left.
    leavingClients <- client
    broadcast <- who + " has left"
}

Serveur TCP

Programmation TCP-IP en Go

const srvAddr = "127.0.0.1:8000"

type clientOutChan chan<- string // per-client outgoing messages

var (
    joiningClients = make(chan clientOutChan) // New clients to be registered
    leavingClients = make(chan clientOutChan) // Leaving clients to be deleted
    broadcast      = make(chan string) // Messages destined to all clients
)

func main() {
    listener, err := net.Listen("tcp", srvAddr)
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()
    
    // Broadcast to all connection
    go broadcaster()
    
    // Wait for new connections
    for {
        conn, err := listener.Accept()
        if err != nil {
            log.Print(err)
        } else {
            go handleConn(conn)
        }
    }
}

func broadcaster() {
    clientOuts := make(map[clientOutChan]bool) // All connected clients
    
    for {
        select {
        case cli := <-joiningClients:
            clientOuts[cli] = true

        case cli := <-leavingClients:
            delete(clientOuts, cli)
            close(cli)
            
        case msg := <-broadcast:
            for cli := range clientOuts {
                cli <- msg
            }
        }
    }
}

func handleConn(conn net.Conn) {
    defer conn.Close()

    client := make(chan string)
    
    // Forward messages from client channel to connection
    go func() {
        for msg := range client {
            fmt.Fprintln(conn, msg)
        }
    }()
    
    // Send welcome message to client
    who := conn.RemoteAddr().String()
    client <- "You are " + who
    
    // Broadcasting arrival of new client
    broadcast <- who + " has arrived"

    joiningClients <- client
    
    // Broadcasting client messages
    input := bufio.NewScanner(conn)
    for input.Scan() {
        broadcast <- who + " says: " + input.Text()
    }
    
    // No more messages, client left.
    leavingClients <- client
    broadcast <- who + " has left"
}

Serveur TCP

Programmation TCP-IP en Go

const srvAddr = "127.0.0.1:8000"

type clientOutChan chan<- string // per-client outgoing messages

var (
    joiningClients = make(chan clientOutChan) // New clients to be registered
    leavingClients = make(chan clientOutChan) // Leaving clients to be deleted
    broadcast      = make(chan string) // Messages destined to all clients
)

func main() {
    listener, err := net.Listen("tcp", srvAddr)
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()
    
    // Broadcast to all connection
    go broadcaster()
    
    // Wait for new connections
    for {
        conn, err := listener.Accept()
        if err != nil {
            log.Print(err)
        } else {
            go handleConn(conn)
        }
    }
}

func broadcaster() {
    clientOuts := make(map[clientOutChan]bool) // All connected clients
    
    for {
        select {
        case cli := <-joiningClients:
            clientOuts[cli] = true

        case cli := <-leavingClients:
            delete(clientOuts, cli)
            close(cli)
            
        case msg := <-broadcast:
            for cli := range clientOuts {
                cli <- msg
            }
        }
    }
}

func handleConn(conn net.Conn) {
    defer conn.Close()

    client := make(chan string)
    
    // Forward messages from client channel to connection
    go func() {
        for msg := range client {
            fmt.Fprintln(conn, msg)
        }
    }()
    
    // Send welcome message to client
    who := conn.RemoteAddr().String()
    client <- "You are " + who
    
    // Broadcasting arrival of new client
    broadcast <- who + " has arrived"

    joiningClients <- client
    
    // Broadcasting client messages
    input := bufio.NewScanner(conn)
    for input.Scan() {
        broadcast <- who + " says: " + input.Text()
    }
    
    // No more messages, client left.
    leavingClients <- client
    broadcast <- who + " has left"
}

Serveur TCP

Programmation TCP-IP en Go

Serveur TCP