Blog Go
Chương trình Go đầu tiên
Brad Fitzpatrick và tôi (Andrew Gerrand) gần đây đã bắt đầu tái cấu trúc godoc, và tôi chợt nhận ra rằng đó là một trong những chương trình Go cổ nhất. Robert Griesemer đã bắt đầu viết nó từ đầu năm 2009, và đến hôm nay chúng tôi vẫn còn dùng nó.
Khi tôi tweet về điều này, Dave Cheney đã trả lời bằng một câu hỏi thú vị: đâu là chương trình Go cổ nhất? Rob Pike đã lục lại thư cũ của mình và tìm thấy nó trong một tin nhắn cũ gửi cho Robert và Ken Thompson.
Những gì dưới đây là chương trình Go đầu tiên. Nó được Rob viết vào tháng 2 năm 2008, khi nhóm chỉ gồm Rob, Robert và Ken. Họ đã có một danh sách tính năng khá đầy đủ (được nhắc tới trong bài viết blog này) và một bản đặc tả ngôn ngữ sơ bộ. Ken vừa hoàn thành phiên bản đầu tiên hoạt động được của trình biên dịch Go (nó chưa sinh mã máy bản địa mà chuyển mã Go sang C để tạo mẫu nhanh) và đã đến lúc thử viết một chương trình bằng nó.
Rob gửi thư cho “nhóm Go”:
From: Rob 'Commander' Pike
Date: Wed, Feb 6, 2008 at 3:42 PM
To: Ken Thompson, Robert Griesemer
Subject: slist
it works now.
roro=% a.out
(defn foo (add 12 34))
return: icounter = 4440
roro=%
here's the code.
some ugly hackery to get around the lack of strings.
(Dòng icounter trong đầu ra của chương trình là số câu lệnh đã được thực thi,
được in ra để phục vụ việc gỡ lỗi.)
package main // fake stuff type char uint8; // const char TESTSTRING[] = "(defn foo (add 'a 'b))\n"; type Atom struct { string *[100]char; integer int; next *Slist; /* in hash bucket */ } type List struct { car *Slist; cdr *Slist; } type Slist struct { isatom bool; isstring bool; //union { atom Atom; list List; //} u; Free method(); Print method(); PrintOne method(doparen bool); String method(*char <-); Integer method(int <-); Car method(*Slist <-); Cdr method(*Slist <-); } method (this *Slist) Car(*Slist <-) { return this.list.car; } method (this *Slist) Cdr(*Slist <-) { return this.list.cdr; } method (this *Slist) String(*[100]char <-) { return this.atom.string; } method (this *Slist) Integer(int <-) { return this.atom.integer; } function OpenFile(); function Parse(*Slist <-); //Slist* atom(char *s, int i); var token int; var peekc int = -1; var lineno int32 = 1; var input [100*1000]char; var inputindex int = 0; var tokenbuf [100]char; var EOF int = -1; // BUG should be const function main(int32 <-) { var list *Slist; OpenFile(); for ;; { list = Parse(); if list == nil { break; } list.Print(); list.Free(); break; } return 0; } method (slist *Slist) Free(<-) { if slist == nil { return; } if slist.isatom { // free(slist.String()); } else { slist.Car().Free(); slist.Cdr().Free(); } // free(slist); } method (slist *Slist) PrintOne(<- doparen bool) { if slist == nil { return; } if slist.isatom { if slist.isstring { print(slist.String()); } else { print(slist.Integer()); } } else { if doparen { print("("); } slist.Car().PrintOne(true); if slist.Cdr() != nil { print(" "); slist.Cdr().PrintOne(false); } if doparen { print(")"); } } } method (slist *Slist) Print() { slist.PrintOne(true); print "\n"; } function Get(int <-) { var c int; if peekc >= 0 { c = peekc; peekc = -1; } else { c = convert(int, input[inputindex]); inputindex = inputindex + 1; // BUG should be incr one expr if c == '\n' { lineno = lineno + 1; } if c == '\0' { inputindex = inputindex - 1; c = EOF; } } return c; } function WhiteSpace(bool <- c int) { return c == ' ' || c == '\t' || c == '\r' || c == '\n'; } function NextToken() { var i, c int; var backslash bool; tokenbuf[0] = '\0'; // clear previous token c = Get(); while WhiteSpace(c) { c = Get(); } switch c { case EOF: token = EOF; case '(': case ')': token = c; break; case: for i = 0; i < 100 - 1; { // sizeof tokenbuf - 1 tokenbuf[i] = convert(char, c); i = i + 1; c = Get(); if c == EOF { break; } if WhiteSpace(c) || c == ')' { peekc = c; break; } } if i >= 100 - 1 { // sizeof tokenbuf - 1 panic "atom too long\n"; } tokenbuf[i] = '\0'; if '0' <= tokenbuf[0] && tokenbuf[0] <= '9' { token = '0'; } else { token = 'A'; } } } function Expect(<- c int) { if token != c { print "parse error: expected ", c, "\n"; panic "parse"; } NextToken(); } // Parse a non-parenthesized list up to a closing paren or EOF function ParseList(*Slist <-) { var slist, retval *Slist; slist = new(Slist); slist.list.car = nil; slist.list.cdr = nil; slist.isatom = false; slist.isstring = false; retval = slist; for ;; { slist.list.car = Parse(); if token == ')' { // empty cdr break; } if token == EOF { // empty cdr BUG SHOULD USE || break; } slist.list.cdr = new(Slist); slist = slist.list.cdr; } return retval; } function atom(*Slist <- i int) { // BUG: uses tokenbuf; should take argument var h, length int; var slist, tail *Slist; slist = new(Slist); if token == '0' { slist.atom.integer = i; slist.isstring = false; } else { slist.atom.string = new([100]char); var i int; for i = 0; ; i = i + 1 { (*slist.atom.string)[i] = tokenbuf[i]; if tokenbuf[i] == '\0' { break; } } //slist.atom.string = "hello"; // BUG! s; //= strdup(s); slist.isstring = true; } slist.isatom = true; return slist; } function atoi(int <-) { // BUG: uses tokenbuf; should take argument var v int = 0; for i := 0; '0' <= tokenbuf[i] && tokenbuf[i] <= '9'; i = i + 1 { v = 10 * v + convert(int, tokenbuf[i] - '0'); } return v; } function Parse(*Slist <-) { var slist *Slist; if token == EOF || token == ')' { return nil; } if token == '(' { NextToken(); slist = ParseList(); Expect(')'); return slist; } else { // Atom switch token { case EOF: return nil; case '0': slist = atom(atoi()); case '"': case 'A': slist = atom(0); case: slist = nil; print "unknown token"; //, token, tokenbuf; } NextToken(); return slist; } return nil; } function OpenFile() { //strcpy(input, TESTSTRING); //inputindex = 0; // (defn foo (add 12 34))\n inputindex = 0; peekc = -1; // BUG EOF = -1; // BUG i := 0; input[i] = '('; i = i + 1; input[i] = 'd'; i = i + 1; input[i] = 'e'; i = i + 1; input[i] = 'f'; i = i + 1; input[i] = 'n'; i = i + 1; input[i] = ' '; i = i + 1; input[i] = 'f'; i = i + 1; input[i] = 'o'; i = i + 1; input[i] = 'o'; i = i + 1; input[i] = ' '; i = i + 1; input[i] = '('; i = i + 1; input[i] = 'a'; i = i + 1; input[i] = 'd'; i = i + 1; input[i] = 'd'; i = i + 1; input[i] = ' '; i = i + 1; input[i] = '1'; i = i + 1; input[i] = '2'; i = i + 1; input[i] = ' '; i = i + 1; input[i] = '3'; i = i + 1; input[i] = '4'; i = i + 1; input[i] = ')'; i = i + 1; input[i] = ')'; i = i + 1; input[i] = '\n'; i = i + 1; NextToken(); }
Chương trình này phân tích và in ra một
S-expression.
Nó không nhận dữ liệu đầu vào từ người dùng và không có import nào, chỉ dựa vào
khả năng print tích hợp sẵn để xuất kết quả.
Nó được viết đúng vào ngày đầu tiên có một
trình biên dịch hoạt động được nhưng còn rất sơ khai.
Phần lớn ngôn ngữ khi ấy chưa được hiện thực và một phần thậm chí còn chưa được đặc tả.
Dù vậy, sắc thái cơ bản của ngôn ngữ ngày nay vẫn có thể nhận ra trong chương trình này. Khai báo kiểu và biến, điều khiển luồng, và câu lệnh package vẫn chưa thay đổi nhiều.
Nhưng cũng có rất nhiều khác biệt và thiếu vắng. Đáng chú ý nhất là sự thiếu vắng concurrency và interface, cả hai đều được xem là thiết yếu ngay từ ngày đầu nhưng khi đó vẫn chưa được thiết kế.
func khi đó là function, và chữ ký của nó chỉ rõ giá trị trả về
trước đối số, ngăn cách bằng <-, thứ mà hiện nay chúng ta dùng như toán tử
gửi/nhận trên channel. Ví dụ, hàm WhiteSpace nhận số nguyên
c và trả về một giá trị boolean.
function WhiteSpace(bool <- c int)
Mũi tên này là một giải pháp tạm thời cho đến khi xuất hiện cú pháp tốt hơn để khai báo nhiều giá trị trả về.
Method tách biệt khỏi function và có từ khóa riêng.
method (this *Slist) Car(*Slist <-) {
return this.list.car;
}
Và method được khai báo trước trong định nghĩa struct, dù điều đó sớm thay đổi.
type Slist struct {
...
Car method(*Slist <-);
}
Không có string, dù chúng đã có trong đặc tả.
Để lách điều đó, Rob phải dựng chuỗi đầu vào như một mảng uint8 với
một cách viết khá vụng về. (Mảng khi đó còn sơ khai và slice vẫn chưa được thiết kế,
chứ chưa nói đến hiện thực, dù đã có khái niệm chưa được hiện thực của
“open array”.)
input[i] = '('; i = i + 1;
input[i] = 'd'; i = i + 1;
input[i] = 'e'; i = i + 1;
input[i] = 'f'; i = i + 1;
input[i] = 'n'; i = i + 1;
input[i] = ' '; i = i + 1;
...
Cả panic và print đều là từ khóa tích hợp sẵn, không phải hàm được khai báo trước.
print "parse error: expected ", c, "\n";
panic "parse";
Và còn nhiều khác biệt nhỏ khác nữa; hãy thử xem bạn có thể nhận ra thêm điều gì.
Chưa đầy hai năm sau khi chương trình này được viết, Go đã được phát hành như một dự án mã nguồn mở. Nhìn lại, thật ấn tượng khi thấy ngôn ngữ đã phát triển và trưởng thành đến mức nào. (Thứ cuối cùng thay đổi giữa proto-Go này và Go mà ta biết ngày nay là việc loại bỏ dấu chấm phẩy.)
Nhưng điều còn ấn tượng hơn là chúng ta đã học được nhiều đến mức nào về việc viết mã Go.
Ví dụ, Rob gọi receiver của method là this, nhưng giờ chúng ta dùng những tên ngắn hơn, phù hợp ngữ cảnh.
Còn hàng trăm ví dụ quan trọng hơn nữa
và cho đến tận hôm nay chúng ta vẫn tiếp tục khám phá ra những cách tốt hơn để viết mã Go.
(Hãy xem mẹo thông minh của gói glog để
xử lý mức độ verbose.)
Tôi tự hỏi ngày mai chúng ta sẽ còn học được điều gì nữa.
Bài tiếp theo: Mảng, slice (và chuỗi): Cơ chế của 'append'
Bài trước: Giới thiệu bộ phát hiện race condition của Go
Mục lục blog