URL: https://linuxfr.org/users/g-bleu/journaux/retour-d-experience-sur-go Title: Retour d'expérience sur Go Authors: G.bleu Date: 2012年05月16日T08:49:03+02:00 License: CC By-SA Tags: golang, linus_torvalds, assembleur et programmation Score: 65 NdM : *Ce journal a été [promu en dépêche](http://linuxfr.org/news/retour-d-experience-sur-go)* Bonjour au journal et à la famille. Je viens de finir un petit projet en Go la semaine dernière. Voici un petit retour d'expérience en espérant que ça serve ! Mise en situation : =================== Pour mes études j'ai un projet (le dernier avant la vie active !) de réalisation de microprocesseur MIPS "from scratch". Le CPU est designé sous Xilinx ISE (grosso modo un IDE dans lequel on peut réaliser des designs de composants à base de portes logiques). Par la suite il sera chargé sur un fpga, celui-ci connecté à un robot afin de le faire suivre une ligne sur le sol. De fait une fois le CPU designé, il faut réaliser un programme en assembleur MIPS, puis le convertir en binaire afin de l'intégrer dans le design du CPU sous la forme d'un module en vhdl. (en gros en entrée du module arrive l'adresse du program counter et le module sort l'instruction correspondante) Les plus attentifs auront déjà pointé du doigt le soucis : comment convertir proprement le code assembleur MIPS en binaire ? - À la main Ne riez pas, c'est ce que m'a proposé mon prof quand je lui ai posé la question ! (à sa décharge, les élèves suivant ce cours ne sont pas informaticiens mais plutôt orientés électronique) - Utiliser un assembleur déjà existant La solution "ne pas réinventer la roue" de référence. Le problème : l'output sera en binaire (logique me direz-vous) mais je veux du code binaire lisible ! (en gros mon output doit être 000101011100110 afin de pouvoir directement copier coller le code dans le fichier de mon module vhddl) Ajouter à cela que je ne veux pas de header ELF ou quoi que ce soit, juste la transcription du code que j'ai écrit. Je pense bien sûr qu'il y a des solutions pour arriver à ce que je veux. Néanmoins j'ai du temps en ce moment et apprendre un nouveau langage me semble plus formateur qu'apprendre les options d'un outil qui ne me servira plus par la suite ! - Écrire un assembleur à la main La solution que j'ai choisi (et qui me donne donc la possibilité de réaliser ce journal !). Un assembleur n'est pas aussi complexe qu'un compilateur (et de loin !) mais permet déjà de s'amuser sur un nouveau langage. Le programme : ============== Linus Torvald :> Show me the code ! [Céans mon bon monsieur !](http://sourceforge.net/projects/gopiler). En espérant que tout le monde aime sourceforge... Comme dit plus haut, il s'agit d'un assembleur MIPS "simplifié" : - Toute les instructions ne sont pas disponibles (pas de jump ou de subi par exemple). La raison est tout simplement que mon processeur ne supporte pas ces instructions et je préfère avoir une erreur à la compilation si j'oublie ce détail que de devoir débugger une erreur qui n'en est pas une par la suite... Malgré tout il est très simple d'ajouter ces fonctionnalités comme nous allons le voir. - Pas de header ELF pour le programme. Comme j'ai dit, l'idée et de copier la sortie de l'assembleur dans un fichier pour que mon cpu l'utilise "tel quel". Pas d'OS, pas le logique supérieur, rien ! Donc pas besoin d'header. Rentrons dans le code --------------------- J'ai divisé mon assembleur en 3 parties : - Le parser/lexer : Réaliser avec yacc pour le premier. On vérifie la grammaire et la sémantique afin de générer une liste d'instructions sous la forme d'un tableau de structures ainsi qu'une map faisant la conversion nom des label => adresse. - Le binder : transforme chaque instruction du tableau précédent en une instruction binaire à proprement parlé (avec vérification en fonction du contexte) - Le main : qui wrap ces deux parties, les connecte entre elles et gère les options fournies par la ligne de commande. Et Go dans tout ça ? -------------------- Il convient avant tout de parler un peu de la philosophie de Go avant d'aller plus loin. En effet l'idée est de fournir des outils tout prêt à l'utilisateur. On se base sur un maximum de règles standards et donc un minimum de configuration (voir en fait pas de configuration du tout dans bien des cas !). ### Le makefile L'exemple le plus frappant de ce concept est le makefile. Si dans les versions antérieurs à la 1.0, Go possédé un simili makefile (il était déjà beaucoup plus simple qu'un makefile typique pour du C), tout cela est révolu ! Maintenant un projet Go n'a besoin pour compiler que de ses sources. Un coup de "go build" et tout se fait tout seul. Plus de gestions des dépendances, plus de problèmes de conflits d'includes (de toute façon, il n'y a pas d'include en Go)... voila qui devrait intéresser, je pense, tout ceux qui se sont essayé à C++ et a ses célèbres erreurs de compilation hyper verbeuses pour cause de conflit de define pour avoir placé un include au mauvais endroit. Petite remarque tout de même : Mon projet contient un makefile ! Bien que minimaliste, celui-ci est donc toujours présent. La raison est multiple : - La commande "go build" construit votre binaire... et c'est tout ! J'aime pouvoir automatiser la génération de tarball, le nettoyage do projet etc... - Mon projet utilise yacc (donc conversion du fichier parser.y en parser.go). De fait, go build ne met pas à jours parser.go si parser.y est mis à jour. d'où la nécessité de gérer cette dépendance ### Les tests De la même manière, réaliser des tests est simplissime. Pour écrire des tests pour le fichier foo.go vous n'avez qu'à créer le fichier foo_test.go et... c'est tout ! Ce fichier se fera automatiquement compiler ses fonctions commençant par Test seront exécuté à chaque lancement de "go test". Dans le fichier, on import le package "testing", et on appel la méthode testing.T.Fail() ou testing.T.Error("C'est la dèche !") pour signaler que le test a échoué : import "testing" func TestFoo(t *testing.T) { // Les fonctions de test commencent par Test // et respectent cette signature if test_is_ok() != nil { t.Error("t'es parti pour fixer ton code !") // Erreur avec message } if test_sans_message != nil { t.Fail() // Erreur sans message } // Si aucun appel à Fail ou Error, alors le test est considéré comme réussit } ### Le multiplatforme ! Encore une très bonne nouvelle : Go est multiplatforme de base ! Voulant partager mon logiciel avec mes petits camarades (Je précise que je suis en Corée du Sud actuellement... pas la peine de dire quel OS utilise tout ce joli monde !), autant dire que cette fonctionnalité a fortement pesé dans la balance pour le choix de Go. De plus, selon la doc il est possible de cross-compiler à partir de n'importe quelle platforme pour n'importe quelle autre juste en changeant ses variables locales comme GOOS ou GOARCH (et bien sûr en compilant sa chaîne de compilation pour l'architecture cible). Toutefois je n'ai pas testé cette possibilité, j'ai préféré rebooter sous windows (à ma décharge, j'aurai de toute façon dû le faire pour vérifier que mon binaire marche bien !) J'ai toutefois trouvé un peu bizarre que la gestion du retour à la ligne ne soit pas fournie comme en C++ (avec std::endl). De fait on doit faire attention à ce léger détail et différencier les cas selon les OS à la main ce qui est assez dommage. ### Les outils en plus En bonus, go fourni des outils des plus sympa : - go fmt Cette commande permet de mettre à LA norme le code. Notez le "LA" majuscule, il n'y en a qu'une (certains diront qu'elle est horrible mais nous ne sommes pas vendredi, je laisse cela à d'autres). Du coup, pas de conflits à ce niveau, tous les codes go écris pas tous les développeurs du monde auront la même forme. Petit bémol pour ma part : cette norme utilise des indentations de 8 caractères et ne coupe pas le code à 80 colonnes. Résultat celui-ci est bien souvent trop long à mon goût (ainsi qu'à celui de mon Emacs en multicolonnes. Là où je peux mettre 3 colonnes en C la plupart du temps, je suis limité à 2 en go... 33% d'espace perdu, snif !) - go tool yacc La commande go tool permet d'accéder à la foultitude d'outils intégré dans la commande go. Parmi eux se trouve yacc, le célèbre parser. Cette version est une réécriture en go de celui de plan9. Autant dire que sa présence à été déterminante dans mon choix d'utiliser go pour mon projet (un parser à la main... non merci !) Écrivons un peu de code ----------------------- Après tout ce temps à parler des outils, parlons du code, du vrai ! ### Le retour de plusieurs variables En voila une bonne idée ! Toute fonction peut renvoyer plusieurs variables au lieu d'une seule dans la plupart des langages. De fait on retrouve dans la lib standard de Go un bon nombre de fonctions renvoyant à la fois la valeur qu'on leur demande ainsi qu'un type "*Error" pouvant être soit "nil" (c'est à dire pointer sur rien) soit initialiser, signifiant alors une erreur. Plus besoins de "tricher" comme en C en donnant en argument de la fonction un pointer sur la variable à compléter puisque la variable de retour est déjà occupé par le code d'erreur. De même le parcours de tableau s'en trouve simplifié : for i,elm := range array { fmt.Println("l'élément", i, "a pour valeur", elm) } range est un mot clé permettant de parcourir un tableau en renvoyant à chaque itération la position ainsi que l'élément courant. Notez aussi le ":=" permettant de déclarer "à l'arrache" des variable en fonction du type de la variable qu'on lui assigne. Un vrai bonheur pour gagner en lisibilité dans les cas triviaux. ### La gestion des options Encore une bonne nouvelle ! La gestions des passages d'options étant une fonctionnalité essentiel à 90% des logiciels, le package "flag" s'occupe de cela avec brio ! import "flag" var f_input = flag.String("i", "", "Input file.") var f_output = flag.String("o", "", "Output file. (stdout if nothing specified)") var f_type = flag.String("t", "vhdl", "Type of output : binary, print, vhdl") func main() { flag.Parse() fmt.Println("lecture du fichier", *f_input) ... } Ai-je vraiment besoin d'expliciter ? On déclare les flags en dehors des fonctions, on appel flag.Parse() avant d'utiliser les flags (notez qu'il s'agit de pointers, on met donc une "*" pour les déférencer) Remarquez que l'option --help/-h est gérée automatiquement ! ### Les conteneurs Go possède de manière intégré au langage les conteneurs les plus utilisés : #### Map Son nom est suffisamment explicite : une clé, une valeur. Problème selon moi : si la clé ne correspond à aucune valeur, la valeur nulle est renvoyée. De fait dans mon programme j'utilise une map pour faire la correspondance entre les labels et leur adresse réelle. Si je demande l'adresse d'un label inexistant, la map va me renvoyer la valeur 0 puisque c'est l'équivalent de la valeur nulle pour un int... Problème : cette valeur peu tout à fait être valide dans le cas d'un label situé en début de code ! C'est d'autant plus étrange que le langage autorise le renvoie de plusieurs valeurs comme nous l'avons vu précédemment... #### Array Un jolie tableau unidimensionnel de taille constante... rien à redire. #### Slice Un type four tout : c'est un genre de tableau à taille variable. En réalité le slice, comme son nom l'indique, représente un morceau de Array. De fait plusieurs slices peuvent pointer en même temps sur le même Array par exemple. Je cherchais à utiliser un vecteur pour stocker chacun de mes instructions une fois parsées/lexées. J'ai eu la surprise de voir que le conteneur Vecteur avait été supprimé de la lib standard il y a quelques commits... La réponse s'est trouvé sur la mailling list de Go : utiliser des slices ! Voici comment faire : var s0 := []int{0, 1} // on créé un un slice sur un tableau contenant 0 et 1 var to_insert = 42 S0 = append(s, to_insert) // on utilise la fonction buildtin "append" Simple ? bon maintenant voyons comment supprimer l'élément numéro i (en C++ j'aurais fait "vect.remove(i)") s0 = append(s[:i - 1], s[i + 1:]...) Pas glop ! Je pense qu'un peu de sucre syntaxique n'aurais pas été de trop pour cacher cette complexité inutile ! Pour ceux qui se posent la question, append ajoute des éléments à un slice. De fait on doit transformer le slice s[i + 1:] en une suit d'élément avant de l'ajouter à s[:i - 1]. C'est ce qu'on fait avec la commande "s[i + 1:]..." ### L'héritage Dans Go l'héritage dans la grande tradition OO n'existe pas ! (et ce n'est pas pour me déplaire à titre personnel) De tout s'articule autour de trois concepts (que je n'illustrerais pas avec mon programme puisse que celui-ci n'en n'utilise pas le premier.) #### les structures Plutôt que de déclarer des classes, on créé des structures. Celles-ci pouvant gérer l'héritage d'une façon amusante : type plat struct { name string } type choucroute struct { _ plat saucisses int } func main() { var ch choucroute = choucroute{ "choucroute", 4 } fmt.Println("Je vais me tapper une", ch.name, "avec ", ch.saucisses, "saucisses dedans !") } On remarque donc que en ajoutant dans choucroute une structure plat de manière anonyme (le nom "_" l'équivalent en Go de John Doe...) les éléments de plat sont intégrés dans choucroute ! #### Les méthodes Une fois notre jolie structure déclarée, il est possible de lui adjoindre des méthodes : func (ch Choucroute)manger() { ch.saucisses-- fmt.Println("Miam ! Encore", ch.saucisses, "saucisses !") } func (_ Plat)manger() { // remarquez le "_" pour indiquer qu'on ne fera rien avec l'objet et qu'il n'est donc pas nécessaire de le nommer. fmt.Println("Beark !") } #### Les interfaces En Go, tout se base sur du Duck typing. De fait afin de pouvoir manger à la fois un plat de seconde zone où de la délicieuse choucroute, on peut déclarer une interface qui contiendra la fonction manger : type mangerer interface { manger() } func main() { bouffe := []mangerer bouffe = append(bouffe, choucroute{ "choucroute", 4 }) bouffe = append(bouffe, plat{ "bouts de tétons de mme Félipé" }) for _, pl := range bouffe { pl.manger() } } De fait le duck typing faisant son travail, notre slice peut contenir n'importe quel structure possédant une fonction ayant pour signature "manger()". Au final un gain de légèreté monstrueux comparé au C++ et à ses déclarations d'objets et d'héritage particulièrement verbeuses. C'est beau, c'est simple, ça me fait pleurer ! ### Les autres fonctionnalités #### les goroutines Bien sûr, je n'ai pas pu tout tester dans mon programme. Je pense notamment à une des principales fonctionnalités mise en avant : la concurrence. Mon programme n'utilise qu'un fil d'exécution. De fait, je n'ai pas pu utiliser les channels ni le mot clé go. Toutefois, ayant déjà fait un peu joujou avec par le passé, j'ai trouvé l'idée vraiment exellente. Les channels permettant à une routine d'attendre qu'une autre lui envoie un objet pour continuer. De fait on résout le soucis de synchronisation de manière beaucoup plus simple qu'en plaçant des mutex sur les ressources critiques ! Conclusion =========== Je suis globalement très satisfait de Go, j'en ai l'impression d'un langage rapide, et ce dans tous les sens du terme : - Rapidité de compilation. Quasi instantanée ! (dois-je comparer à un poid lourd comme C++ ?) - Rapidité de développement. Tout va très vite. Le duck typing permet de redéfinir ses structures très rapidement, on n'écrit que le minimum. De même, Tout le monde est convaincu de l'importance des tests mais la flemme nous fait généralement (en particulier pour les petits projet comme le mien) tester à la main la fonctionnalité sur laquelle on travail actuellement et basta ! De Fait, le système de test intégré à Go est pour moi un pur bonheur tant il est simple est efficace ! - Rapidité d'exécution. Le débat est lancé ! Pour certain le compilateur de Go est trop jeune et pas suffisamment optimisé (ce qui explique sa vitesse de compilation). Les auteurs de Go ont notamment [publié un article](http://blog.golang.org/2011/06/profiling-go-programs.html) pour battre en brêche cette idée en montrant comment obtenir des performances proches du C++ en optimisant son code. D'un autre côté, il s'agit d'un langage jeune, avec tous les problèmes inhérents : - Peu de libs tiers pour le moment Au vu de la simplicité d'interfaçage de Go avec C, des projets de bindings de grosses libs fleurissent un peu partout mais pour le moment rien de très stable/utilisable - Un compilateur jeune. En effet, pour le moment la runtime est compilée statiquement dans le logiciel. Cela explique la taille supérieur au mégaoctet du moindre "hello world". Par exemple mon logiciel faisant dans les 800 lignes de Go se retrouve compilé dans un binaire de 1.6mo... Enfin bon vu la taille actuelle de nos disques durs et tant que Go n'aurra pas pour vocation de tourner sur de l'embarqué, je ne suis pas sûr que ce soit un si gros point noir.

AltStyle によって変換されたページ (->オリジナル) /