Bài trước nói Go không có ép kiểu ngầm. Hình dung một con số viết trên cái nhãn: nó có thể là một số lượng ("65 món") hoặc một mã tra bảng ("ô số 65" — như số tủ đồ, mã vùng, mã ký tự). Go buộc bạn nói rõ đang hiểu theo nghĩa nào, và đây là ba chỗ người đến từ Java hay Python hay vấp.

Cắt cụt, không làm tròn

  int(3.99)  =  3
  int(-3.99) = -3      <- cắt về phía 0, không phải làm tròn xuống
  int8(300)  = 44      <- cuộn vòng, không lỗi

Hai điều cần nhớ.

int(x) cắt phần thập phân, và cắt về phía 0 — nên int(-3.99) là -3 chứ không phải -4. Như một người thư ký máy móc cầm kéo xén thẳng phần sau dấu phẩy, không bao giờ làm tròn. Muốn làm tròn thì math.Round, muốn xuống thì math.Floor.

Chuyển sang kiểu hẹp hơn thì cuộn vòng im lặng. int8(300) cho 44 vì 300 vượt biên int8 — cái mặt đồng hồ đếm chỉ tới một mức rồi quay vòng về đầu. Trình biên dịch không cản, go vet không cản. Đây là chỗ Go cố tình không bảo vệ bạn — cùng lý do với tràn số ở bài trước.

Khi đọc dữ liệu từ ngoài vào kiểu hẹp, hãy kiểm biên tường minh trước khi chuyển.

string(65) không phải "65"

Đây là cái bẫy tôi thấy người mới mắc nhiều nhất.

  string(rune(65)) = "A"

Chuyển một số nguyên sang string trong Go nghĩa là "lấy ký tự Unicode có mã đó", không phải "viết số đó ra chữ". string(65) cho "A" vì 65 là mã của chữ A — Go đọc cái nhãn theo nghĩa mã tra bảng, tra ô số 65 trong bảng ký tự, chứ không đánh vần con số.

Từ Go 1.15, trình biên dịch cảnh báo khi bạn viết string(i) với i là int — nhưng nó vẫn biên dịch được. Với rune thì không cảnh báo, vì đó là cách dùng đúng.

Muốn đổi số thành chuỗi — đánh vần con số ra — thì dùng strconv:

strconv.Itoa(65)                     // "65"
strconv.FormatFloat(3.14, 'f', 2, 64) // "3.14"
fmt.Sprintf("%d", 65)                 // "65", chậm hơn nhưng linh hoạt

Và chiều ngược lại luôn trả kèm lỗi:

  strconv.Atoi("42")    = 42, err=<nil>
  strconv.Atoi("42abc") -> err = strconv.Atoi: parsing "42abc": invalid syntax

Không có phiên bản "phân tích rồi ném exception" như Integer.parseInt của Java. Bạn buộc phải xử lý lỗi, và đó là chủ đề của bài 23.

Cái gì so sánh được bằng ==

  struct so được : true
  mảng  so được  : true
  slice / map / func : LỖI BIÊN DỊCH

Go chia kiểu thành so sánh được và không so sánh được, và ranh giới không giống Java chút nào.

So sánh được: số, chuỗi, bool, con trỏ, channel, interface, mảng, và struct nếu mọi trường của nó so sánh được. Diem{1,2} == Diem{1,2} cho true — không cần viết equals, không cần hashCode. Đây là thứ Java phải chờ tới record mới có.

Không so sánh được: slice, map, function. Chúng chỉ so được với nil. Lý do: so sánh chúng hoặc rất tốn kém, hoặc không có định nghĩa rõ ràng.

Hệ quả quan trọng: struct chứa slice thì không dùng làm khoá map được, và trình biên dịch sẽ báo lỗi ngay. Muốn so sánh sâu thì reflect.DeepEqual, nhưng nó chậm và không nên nằm trong đường chạy nóng.

Vài chỗ khác của toán tử

Không có ++i, chỉ có i++, và nó là câu lệnh chứ không phải biểu thức. x := i++ là lỗi biên dịch.

Không có toán tử ba ngôi. Không có a ? b : c. Phải viết if. Đây là quyết định gây tranh cãi nhất của Go, và lý do chính thức là "biểu thức ba ngôi lồng nhau khó đọc".

% chỉ dùng cho số nguyên. Với float thì math.Mod.

Toán tử bit có thêm &^ — AND NOT, để xoá bit. Hiếm gặp ở ngôn ngữ khác.

&& và || vẫn đoản mạch như Java.

Nếu chỉ thử một thứ sau bài này, đặt hai cách đọc cái nhãn cạnh nhau trong ba mươi giây:

fmt.Println(string(rune(9731)))
fmt.Println(strconv.Itoa(9731))

Dòng đầu in ra một hình người tuyết (ô số 9731 trong bảng ký tự), dòng sau in ra 9731 (con số đánh vần ra). Ba mươi giây để nhớ vĩnh viễn rằng string(số) và strconv.Itoa(số) là hai việc hoàn toàn khác nhau.

Mẫu số chung

Chuyển kiểu là một hành động có chủ đích, chính xác, đôi khi mất mát, và một ngôn ngữ từ chối đoán ý buộc bạn phải biết cái quy tắc. Go chuyển tường minh và đúng-nghĩa-đen — cắt về phía 0, cuộn vòng im lặng khi thu hẹp, đọc một int như một điểm mã — trong khi các ngôn ngữ "biết điều" lại giấu những phép ép khiến bạn dính đòn: "5" + 3 của JavaScript, thu hẹp ngầm của C, chia số nguyên của Python 2. Ép kiểu ngầm đổi chút công gõ phím lấy cả một lớp bọ âm thầm; ép kiểu tường minh làm cái mất mát hiện ra ngay tại chỗ nó xảy ra — đó là lý do phải kiểm biên dữ liệu ngoài trước khi thu hẹp. Lại là điệp khúc của sê-ri: không bất ngờ âm thầm, cái mất mát phải được nói thành lời.

Điều thứ hai: hai thao tác trông giống nhau có thể làm hai việc ngược nhau, và bạn phải nói mình muốn cái nào — string(65) đọc một điểm mã ("A"), strconv.Itoa(65) đánh vần con số ("65"); một con số là số lượng hay mã, một đoạn text là ký tự hay chữ số. Đúng cái ranh giới dữ-liệu-so-với-biểu-diễn như thoát ký tự template và enum-là-chuỗi ở bài REST, và chiều phân tích luôn trả kèm lỗi vì biến text không tin được thành số có thể hỏng (không có parseInt im lặng). Bằng nhau cũng đòi cùng cái "biết nó thật ra làm gì": == là so theo cấu trúc với struct và mảng, bị cấm thẳng với slice và map vì không có nghĩa rẻ và rõ — cùng bài học biết phép so của mình như so Time bằng Equal. Chuyển tường minh, xử lý lỗi phân tích, và biết == đang so giá trị hay thậm chí có được phép không.

Ngày mai: if, switch và for — Go chỉ có một từ khoá cho mọi vòng lặp.