Chúng tôi đã giới thiệu giai đoạn đầu tiên của biên dịch ngôn ngữ Go trong phần trước Golang Lexer and Parser, - AST được thực xây dựng. Tiếp tục với giai đoạn tiếp theo của việc thực thi trình biên dịch - kiểm tra kiểu.
Đề cập đến các hệ thống kiểu kiểm tra kiểu và ngôn ngữ lập trình, nhiều bạn có thể nghĩ đến một số thuật ngữ hơi mơ hồ và khó hiểu: kiểu mạnh, kiểu yếu, kiểu tĩnh và kiểu động. Nhưng bây giờ chúng ta phải nói về quá trình kiểm tra kiểu cura trình biên dịch ngôn ngữ Go, chúng ta sẽ hoàn toàn tìm ra ý nghĩa của những kiểu này và tương tự.
1. Strong and Weak Type
Strong and weak typethường được thảo luận với nhau, nhưng cả hai đều không có một định nghĩa học thuật nghiêm ngặt, truy cập nhiều tài liệu hơn để hiểu lại trở khó hiểu hơn, nhiều tài liệu thậm chí mâu thuẫn với nhau.
Do sự không đầy đủ của định nghĩa của thẩm quyền, đối với các kiểu mạnh hay yếu, nhiều lần chúng ta chỉ có thể dựa trên hiện tượng và đặc điểm từ trực giác để đánh giá, nói chung sẽ có kết luận sau đây:
- Các ngôn ngữ lập trình kiểu mạnh có những hạn chế kiểu nghiêm ngặt hơn trong quá trình biên dịch, tức là trình biên dịch sẽ tìm thấy type error khi gán biến, giá trị trả về và gọi hàm trong quá trình biên dịch;
- Các ngôn ngữ lập trình kiểu yếu có thể thực hiện chuyển đổi kiểu ngầm tại runtime khi gặp type error và có thể gây ra lỗi chạy khi chuyển đổi kiểu.
Dựa trên kết luận trên, chúng ta có thể nghĩ rằng Java, C# và các ngôn ngữ lập trình khác thực hiện kiểm tra kiểu trong quá trình biên dịch là các kiểu mạnh. Tương tự như vậy, bởi vì ngôn ngữ Go tìm thấy type error trong quá trình biên dịch, nó nên là một kiểu ngôn ngữ lập trình mạnh.
Nếu định nghĩa khái niệm của các kiểu mạnh và yếu không nghiêm ngặt và mơ hồ, thì về mặt khái niệm, bản thân nó không có nhiều giá trị thực tế, ít nhất là ít hữu ích cho việc chúng ta thực sự sử dụng và hiểu ngôn ngữ lập trình. Câu hỏi đặt ra, như một định nghĩa trừu tượng, chúng ta sử dụng nó để làm gì? Câu trả thường là sự tiện lợi của kiện giao tiếp và phân lớp giữa các kiểu. Hãy bỏ qua các kiểu mạnh hay yếu và tập trung nhiều hơn vào các câu hỏi sau:
- Chuyển đổi kiểu là rõ ràng hay ngầm?
- Trình biên dịch có giúp chúng ta suy ra kiểu biến không?
Những vấn đề cụ thể này thực sự có giá trị hơn trong bối cảnh này, và hy vọng rằng độc giả có thể giảm tranh chấp về các kiểu mạnh và kiểu yếu.
2. Static and Dynamic Type
Các ngôn ngữ lập trình kiểu tĩnh (static) và kiểu động (dynamic) thực sự là không chính xác. Chính xác nên là ngôn ngữ lập trình sử dụng Static Type Checking và Dynamic Type Checking, phần này giới thiệu các đặc điểm của hai kiểu kiểm tra và sự khác biệt của chúng.
Static Type Checking
Static Type Checking: Kiểm tra kiểu tĩnh là một quá trình dựa trên phân tích mã nguồn để xác định kiểu chương trình chạy an toàn. Nếu mã của chúng tôi có thể vượt qua kiểm tra kiểu tĩnh, chương trình hiện tại có thể đáp ứng các yêu cầu bảo mật kiểu ở một mức độ nào đó, nó có thể làm giảm kiểm tra kiểu chương trình tại runtime hoặc có thể được coi là một cách để tối ưu hóa mã.
Là một developer, kiểm tra kiểu tĩnh có thể giúp chúng tôi phát hiện ra type error xảy ra trong quá trình biên dịch và một số ngôn ngữ lập trình kiểu động có các công cụ được cung cấp bởi cộng đồng để thêm kiểm tra kiểu tĩnh cho các ngôn ngữ lập trình này, chẳng hạn như Flow cho JavaScript, những công cụ này có thể phát hiện type error trong mã trong quá trình biên dịch.
Tôi tin rằng rất nhiều độc giả cũng đã nghe nói “Dynamic type is cool for a while, code refactoring crematorium”. Các developer sử dụng Python, Ruby và các ngôn ngữ lập trình khác phải có kinh nghiệm sâu sắc về cụm từ này, kiểu tĩnh cung cấp những hạn chế cho mã trong quá trình biên dịch, trình biên dịch có thể hạn chế các kiểu biến trong quá trình biên dịch.
Kiểm tra kiểu tĩnh có thể giúp chúng tôi tiết kiệm rất nhiều thời gian và tránh bỏ lỡ khi tái cấu trúc, nhưng nếu ngôn ngữ lập trình chỉ hỗ trợ kiểm tra kiểu động, bạn sẽ cần phải viết một số lượng lớn các unit test để đảm bảo rằng việc tái cấu trúc không gặp type error. Tất nhiên ở đây không phải là thử nghiệm không quan trọng, bất kỳ mã nào cũng nên có một bài kiểm tra tốt, điều này không liên quan nhiều đến ngôn ngữ.
Dynamic Type Checking
Dynamic Type Checking: Kiểm tra kiểu động là quá trình xác định kiểu chương trình an toàn tại runtime, đòi hỏi ngôn ngữ lập trình thêm thông tin như nhãn kiểu cho tất cả các đối tượng tại thời điểm biên dịch và runtime có thể sử dụng thông tin kiểu được lưu trữ này để dynamic dispatch, downcasting, reflection và các tính năng khác. Kiểm tra kiểu động cung cấp cho các kỹ sư nhiều không gian hoạt động hơn, cho phép chúng tôi có được một số ngữ cảnh liên quan đến kiểu và thực hiện một số hành động động dựa trên kiểu đối tượng tại runtime.
Các ngôn ngữ lập trình chỉ sử dụng kiểm tra kiểu động được gọi là ngôn ngữ lập trình kiểu động, các ngôn ngữ lập trình kiểu động phổ biến bao gồm JavaScript, Ruby và PHP, mặc dù các ngôn ngữ lập trình này rất linh hoạt trong sử dụng và không cần phải được biên dịch, mã có vấn đề sẽ không làm giảm lỗi vì tính linh hoạt hơn, lỗi vẫn có thể chạy. Trong khi tăng tính linh hoạt, chúng cũng tăng yêu cầu chất lượng kỹ sư.
Summary
Kiểm tra kiểu tĩnh và kiểm tra kiểu động không hoàn toàn xung đột và đối lập, nhiều ngôn ngữ lập trình sử dụng cả hai kiểu kiểm tra cùng một lúc, chẳng hạn như Java không chỉ kiểm tra type error trước trong quá trình biên dịch, mà còn thêm thông tin kiểu cho các đối tượng, sử dụng reflect tại runtime để tự động thực thi hàm theo kiểu đối tượng để tăng tính linh hoạt và giảm mã dự phòng.
3. Execution process
Trình biên dịch ngôn ngữ Go không chỉ sử dụng kiểm tra kiểu tĩnh để giữ an toàn cho kiểu chương trình chạy, mà còn giới thiệu thông tin kiểu trong quá trình lập trình, cho phép các kỹ sư sử dụng reflect để xác định kiểu tham số và biến. Khi chúng tôi muốn chuyển đổi interface{}
thành một kiểu cụ thể, chúng tôi thực hiện kiểm tra kiểu động và nếu không có chuyển đổi xảy ra, chương trình sụp đổ.
Ở đây chúng tôi sẽ tập trung vào việc kiểm tra kiểu tĩnh trong quá trình biên dịch, và trong Golang Compile Intro, chúng tôi đã giới thiệu cmd/compile/internal/gc.Main
, một trong số đó là như thế này:
Quá trình thực hiện mã này có thể được chia thành hai phần, bắt đầu bằng cách kiểm tra constant, type, function declaration, and variable assignment statement thông qua hàm cmd/compile/internal/gc.typecheck
. Sau đó sử dụng cmd/compile/internal/gc.checkMapKeys
để kiểm tra kiểu hash map key, chúng tôi sẽ phân tích nguyên tắc thực hiện mã trên trong một số phần.
Logic chính của kiểm tra kiểu trình biên dịch đều ở cmd/compile/internal/gc.typecheck
và cmd/compile/internal/gc.typecheck1
. Điều này trongcmd/compile/internal/gc.typecheck
không phải là nhiều, nó sẽ làm một số kiểu kiểm tra trước khi chuẩn bị. Logic cốt lõi nằm trong cmd/compile/internal/gc.typecheck1
, một hàm 2000 dòng switch/case:
cmd/compile/internal/gc.typecheck1
vào các nhánh khác nhau dựa trên kiểu nút đến Op, bao gồm hơn 150 kiểu thao tác như nhân cộng và trừ, nhân, chia, function call, method call, v.v., vì có rất nhiều kiểu nút, vì vậy chỉ có một vài trường hợp điển hình được trích dẫn để phân tích chuyên sâu ở đây.
Slice OTARRAY
Nếu kiểu hoạt động của nút hiện tại là OTARRAY
, nhánh này bắt đầu bằng cách kiểm tra kiểu các nút bên phải, cụ thể là kiểu các yếu tố slice
hoặc array
:
cmd/compile/internal/gc.Node
, tức là ba cách khai báo khác nhau []int
,[…]int
và [3]int
. Kiểu đầu tiên tương đối đơn giản, sẽ gọi cmd/compile/internal/types.NewSlice
:
cmd/compile/internal/types.NewSlice
trả về trực tiếp một struct kiểu TSLICE
và thông tin kiểu phần tử được lưu trữ trong struct. Khi gặp […]int
, kiểu mảng này được xử lý bởi cmd/compile/internal/gc.typecheckcomplit
:
Cuối cùng, nếu mã nguồn chứa kích thước của mảng, cmd/compile/internal/types.NewArray
khởi tạo một cấu trúc lưu trữ kiểu phần tử mảng và kích thước mảng:
Ba nhánh khác nhau xử lý các hình thức khác nhau của khai báo array
và slice
, mỗi nhánh được cập nhật cmd/compile/internal/gc.Node
Các kiểu được lưu trữ trong cấu trúc Node và sửa đổi nội dung trong cây cú pháp trừu tượng. Thông qua phân tích của clip này, chúng tôi thấy rằng độ dài của mảng được xác định trong quá trình kiểm tra kiểu, trong khi […]int
thức khai báo này của int cũng chỉ là đường ngữ pháp mà ngôn ngữ Go cung cấp cho chúng tôi.
Hash OTMAP
Nếu các nút được xử lý là hash, trình biên dịch kiểm tra các kiểu giá trị key của hash riêng biệt để xác minh tính hợp lệ của các kiểu của chúng:
Gần như giống hệt với khi xử lý slice, nó sẽ đi qua cmd/compile/internal/types.NewMap
tạo ra một cấu trúc TMAP
mới và lưu kiểu key-value vào cấu trúc đó:
Các nút đại diện cho hash hiện tại cuối cùng cũng sẽ được thêm vào hàng đợi mapqueue
, và trình biên dịch sẽ kiểm tra lại kiểu hash key ở giai đoạn sau, trong khi kiểm tra kiểu key thực sự được gọi là hàm cmd/compile/internal/gc.checkMapKeys
được đề cập ở trên:
Hàm này đi qua các nút đang chờ kiểm tra trong hàng đợi mapqueue
để xác định xem các kiểu này có thể hoạt động như các hash key hay không và nếu kiểu hiện tại không hợp lệ, nó sẽ trực tiếp báo lỗi cho toàn bộ quá trình kiểm tra trong giai đoạn kiểm tra kiểu.
Keyword OMAKE
Cuối cùng, giới thiệu make
một hàm tích hợp phổ biến trong ngôn ngữ Go, trước giai đoạn kiểm tra kiểu, cho dù đó là slice, hash hoặc channel sử dụng từ khóa make
, nhưng trong giai đoạn kiểm tra kiểu make
sẽ bị thay thay thế bằng một hàm cụ thể dựa trên kiểu được tạo ra và quá trình tạo mã trung gian (Golang IR SSA) sau này sẽ không còn OMAKE
. Các nút của kiểu OMAKE
được xử lý dựa trên kiểu phân đoạn được tạo ra:
Trình biên dịch kiểm tra lần đầu các tham số kiểu đầu tiên của make
, đi vào các nhánh khác nhau tùy thuộc vào kiểu, các nhánh slice TSLICE
, nhánh hash TMAP
và nhánh channel TCHAN
:
Nếu tham số đầu tiên của make
là kiểu slice, bạn sẽ lấy độ dài len
của slice và dung lượng cap
từ các tham số và kiểm tra cả hai tham số, bao gồm:
- Cho dù các tham số chiều dài của slice được truyền vào;
- Chiều dài của slice phải nhỏ hơn hoặc bằng dung lượng của slice
Ngoài việc kiểm tra số lượng và tính hợp lệ của các tham số, mã này cuối cùng sẽ thay đổi op hoạt động của nút hiện tại thành OMAKESLICE
để tạo điều kiện xử lý giai đoạn biên dịch tiếp theo.
Trường hợp thứ hai là tham số đầu tiên của make
là kiểu map
, trong trường hợp đó tham số tùy chọn thứ hai là kích thước ban đầu của hash, theo mặc định kích thước của nó là 0 và nhánh hiện tại cuối cùng sẽ thay đổi thuộc tính Op của nút hiện tại:
Cấu trúc cuối cùng mà hàm tích hợp make
có thể khởi tạo là Channel (Golang Channel), từ mã sau đây, chúng ta có thể thấy rằng tham số thứ hai đại diện cho kích thước bộ đệm của Channel, và nếu không có tham số thứ hai, channel có kích thước bộ đệm là 0:
Trong quá trình kiểm tra kiểu, bất kể kiểu tham số đầu tiên của make
, kiểu Op của nút hiện tại được sửa đổi và một số xác minh về tính hợp lệ của các tham số đến.
4. Review
Kiểm tra kiểu là giai đoạn thứ hai của biên dịch ngôn ngữ Go, sau khi phân tích từ và ngữ pháp, chúng tôi nhận được AST tương ứng với mỗi tập tin, sau đó kiểm tra kiểu sẽ đi qua các nút trong cây ngữ pháp trừu tượng, kiểm tra kiểu của mỗi nút, tìm ra lỗi ngữ pháp tồn tại trong đó, trong quá trình này cũng có thể viết lại AST, điều này không chỉ có thể loại bỏ một số mã sẽ không được thực thi, tối ưu hóa mã để cải thiện hiệu quả thực thi, mà còn sửa đổi make
, new
và các từ khóa khác tương ứng với kiểu hoạt động của nút.
make
và new
các chức năng tích hợp này thực sự không tương ứng trực tiếp với việc thực hiện một số chức năng nhất định, chúng sẽ được chuyển đổi thành các hàm khác thực sự tồn tại trong quá trình biên dịch, và chúng tôi sẽ mô tả những gì trình biên dịch đã làm cho chúng trong phần tiếp theo của tạo mã trung gian (Golang IR SSA).