Tại sao Go lại không tốt đối với những lập trình viên không thông minh

Bài báo được viết như một phản hồi cho một bài viết được xuất bản trước đó bài viết phản đối.

Tại sao Go lại không tốt đối với những lập trình viên không thông minh

Trong hơn hai năm qua, tôi đã sử dụng Go để triển khai máy chủ RADIUS chuyên dụng với hệ thống thanh toán được phát triển. Trong quá trình đó, tôi đang tìm hiểu sự phức tạp của ngôn ngữ này. Bản thân các chương trình rất đơn giản và không phải là mục đích của bài viết này, nhưng bản thân trải nghiệm sử dụng cờ vây xứng đáng nhận được vài lời bảo vệ. Go đang trở thành một ngôn ngữ ngày càng phổ biến dành cho mã nghiêm túc, có khả năng mở rộng. Ngôn ngữ được tạo bởi Google và được sử dụng tích cực. Tóm lại, tôi thành thật nghĩ rằng thiết kế của ngôn ngữ Go không tốt cho các lập trình viên UNintelligent.

Được thiết kế cho các lập trình viên yếu?

Kẻ yếu nói về vấn đề. Cuộc nói chuyện mạnh mẽ về ý tưởng và ước mơ...

Go rất dễ học, dễ đến mức bạn có thể đọc mã mà hầu như không cần đào tạo gì cả. Tính năng này của ngôn ngữ được sử dụng ở nhiều công ty toàn cầu khi mã được đọc cùng với các chuyên gia không cốt lõi (người quản lý, khách hàng, v.v.). Điều này rất thuận tiện cho các phương pháp như Phát triển theo hướng thiết kế.
Ngay cả những lập trình viên mới vào nghề cũng bắt đầu tạo ra được mã khá tốt sau một hoặc hai tuần. Cuốn sách tôi học là “Go Programming” (của Mark Summerfield). Sách rất hay, chạm đến nhiều sắc thái của ngôn ngữ. Sau những ngôn ngữ phức tạp không cần thiết như Java, PHP, sự thiếu vắng phép thuật đang được làm mới. Nhưng sớm hay muộn, nhiều lập trình viên hạn chế cũng có ý tưởng sử dụng các phương pháp cũ trong một lĩnh vực mới. Điều này có thực sự cần thiết không?

Rob Pike (nhà tư tưởng chính của ngôn ngữ) đã tạo ra ngôn ngữ Go như một ngôn ngữ công nghiệp dễ hiểu và hiệu quả khi sử dụng. Ngôn ngữ này được thiết kế để đạt năng suất tối đa trong các nhóm lớn và không có nghi ngờ gì về điều đó. Nhiều lập trình viên mới vào nghề phàn nàn rằng họ còn thiếu nhiều tính năng. Mong muốn về sự đơn giản này là một quyết định có ý thức của các nhà thiết kế ngôn ngữ và để hiểu đầy đủ lý do tại sao nó lại cần thiết, chúng ta phải hiểu động lực của các nhà phát triển và những gì họ đang cố gắng đạt được trong Go.

Vậy tại sao nó lại được thực hiện đơn giản như vậy? Dưới đây là một vài trích dẫn từ Rob Pike:

Điểm mấu chốt ở đây là các lập trình viên của chúng tôi không phải là nhà nghiên cứu. Theo quy luật, họ còn khá trẻ, đến với chúng tôi sau khi học xong, có lẽ họ đã học Java, C/C++ hoặc Python. Họ không thể hiểu được một ngôn ngữ tuyệt vời, nhưng đồng thời chúng tôi muốn họ tạo ra phần mềm tốt. Đó là lý do tại sao ngôn ngữ phải dễ hiểu và dễ học.

Chắc anh này quen lắm, nói đại khái giống C. Các lập trình viên làm việc tại Google bắt đầu sự nghiệp của họ sớm và hầu hết đều quen thuộc với các ngôn ngữ thủ tục, đặc biệt là họ C. Yêu cầu về năng suất nhanh trong ngôn ngữ lập trình mới có nghĩa là ngôn ngữ đó không nên quá cấp tiến.

Những lời nói khôn ngoan phải không?

Hiện vật của sự đơn giản

Đơn giản là điều kiện cần của cái đẹp. Lev Tolstoi.

Giữ nó đơn giản là một trong những mục tiêu quan trọng nhất trong bất kỳ thiết kế nào. Như bạn đã biết, một dự án hoàn hảo không phải là một dự án không có gì để thêm vào mà là một dự án không có gì phải bỏ đi. Nhiều người tin rằng để giải quyết (hoặc thậm chí diễn đạt) các vấn đề phức tạp thì cần có một công cụ phức tạp. Tuy nhiên, nó không phải vậy. Hãy lấy ngôn ngữ PERL làm ví dụ. Các nhà tư tưởng ngôn ngữ tin rằng một lập trình viên nên có ít nhất ba cách khác nhau để giải quyết một vấn đề. Các nhà tư tưởng của ngôn ngữ cờ vây đã đi theo một con đường khác; họ quyết định rằng chỉ một cách, nhưng thực sự tốt, là đủ để đạt được mục tiêu. Cách tiếp cận này có một nền tảng nghiêm túc: cách duy nhất dễ học hơn và khó quên hơn.

Nhiều người di cư phàn nàn rằng ngôn ngữ này không chứa đựng những sự trừu tượng tao nhã. Vâng, điều này đúng, nhưng đây là một trong những ưu điểm chính của ngôn ngữ. Ngôn ngữ chứa đựng tối thiểu phép thuật - vì vậy không cần kiến ​​thức sâu để đọc chương trình. Đối với tính dài dòng của mã, đây không phải là vấn đề gì cả. Một chương trình Golang được viết tốt sẽ đọc theo chiều dọc, có rất ít hoặc không có cấu trúc. Ngoài ra, tốc độ đọc chương trình ít nhất phải lớn hơn tốc độ ghi chương trình đó một bậc. Nếu bạn cho rằng tất cả mã đều có định dạng thống nhất (được thực hiện bằng lệnh gofmt tích hợp), thì việc đọc thêm một vài dòng không phải là vấn đề gì cả.

Không biểu cảm lắm

Nghệ thuật không chấp nhận khi sự tự do của nó bị hạn chế. Độ chính xác không phải là trách nhiệm của anh ấy.

Do mong muốn sự đơn giản, Go thiếu các cấu trúc mà trong các ngôn ngữ khác được những người quen với chúng coi là điều gì đó tự nhiên. Lúc đầu, nó có thể hơi bất tiện, nhưng sau đó bạn nhận thấy rằng chương trình này dễ đọc và rõ ràng hơn nhiều.

Ví dụ: tiện ích bảng điều khiển đọc stdin hoặc tệp từ đối số dòng lệnh sẽ trông như thế này:

package main

import (
    "bufio"
    "flag"
    "fmt"
    "log"
    "os"
)

func main() {

    flag.Parse()

    scanner := newScanner(flag.Args())

    var text string
    for scanner.Scan() {
        text += scanner.Text()
    }

    if err := scanner.Err(); err != nil {
        log.Fatal(err)
    }

    fmt.Println(text)
}

func newScanner(flags []string) *bufio.Scanner {
    if len(flags) == 0 {
        return bufio.NewScanner(os.Stdin)
    }

    file, err := os.Open(flags[0])

    if err != nil {
        log.Fatal(err)
    }

    return bufio.NewScanner(file)
}

Lời giải cho cùng một vấn đề ở D, mặc dù có vẻ ngắn hơn một chút nhưng lại không dễ đọc hơn

import std.stdio, std.array, std.conv;

void main(string[] args)
{
    try
    {
        auto source = args.length > 1 ? File(args[1], "r") : stdin;
        auto text   = source.byLine.join.to!(string);

        writeln(text);
    }
    catch (Exception ex)
    {
        writeln(ex.msg);
    }
}

Địa ngục sao chép

Con người mang địa ngục bên trong mình. Martin Luther.

Những người mới bắt đầu liên tục phàn nàn về cờ vây vì thiếu thông tin cơ bản. Để giải quyết vấn đề này, hầu hết họ đều sử dụng cách sao chép mã trực tiếp. Ví dụ: một hàm tính tổng một danh sách các số nguyên, những chuyên gia tương lai như vậy tin rằng chức năng này không thể được triển khai theo bất kỳ cách nào khác ngoài việc sao chép-dán đơn giản cho từng loại dữ liệu.

package main

import "fmt"

func int64Sum(list []int64) (uint64) {
    var result int64 = 0
    for x := 0; x < len(list); x++ {
        result += list[x]
    }
    return uint64(result)
}

func int32Sum(list []int32) (uint64) {
    var result int32 = 0
    for x := 0; x < len(list); x++ {
        result += list[x]
    }
    return uint64(result)
}

func main() {

    list32 := []int32{1, 2, 3, 4, 5}
    list64 := []int64{1, 2, 3, 4, 5}

    fmt.Println(int32Sum(list32))
    fmt.Println(int64Sum(list64))
}

Ngôn ngữ có đủ phương tiện để thực hiện những công trình như vậy. Ví dụ, lập trình chung sẽ ổn.

package main

import "fmt"

func Eval32(list []int32, fn func(a, b int32)int32) int32 {
    var res int32
    for _, val := range list {
        res = fn(res, val)
    }
    return res
}

func int32Add(a, b int32) int32 {
    return a + b
}

func int32Sub(a, b int32) int32 {
    return a + b
}

func Eval64(list []int64, fn func(a, b int64)int64) int64 {
    var res int64
    for _, val := range list {
        res = fn(res, val)
    }
    return res
}

func int64Add(a, b int64) int64 {
    return a + b
}

func int64Sub(a, b int64) int64 {
    return a - b
}

func main() {

    list32 := []int32{1, 2, 3, 4, 5}
    list64 := []int64{1, 2, 3, 4, 5}

    fmt.Println(Eval32(list32, int32Add))
    fmt.Println(Eval64(list64, int64Add))
    fmt.Println(Eval64(list64, int64Sub))
}

Và, mặc dù mã của chúng tôi hóa ra dài hơn một chút so với trường hợp trước, nhưng nó đã trở nên khái quát. Vì vậy, sẽ không khó để chúng ta thực hiện được mọi phép tính số học.

Nhiều người sẽ nói rằng chương trình ở dạng D trông ngắn hơn đáng kể và họ sẽ đúng.

import std.stdio;
import std.algorithm;

void main(string[] args)
{
    [1, 2, 3, 4, 5].reduce!((a, b) => a + b).writeln;
}

Tuy nhiên, nó chỉ ngắn hơn chứ không chính xác hơn vì việc triển khai D hoàn toàn bỏ qua vấn đề xử lý lỗi.

Trong cuộc sống thực, khi độ phức tạp của logic tăng lên, khoảng cách sẽ thu hẹp nhanh chóng. Khoảng cách thậm chí còn thu hẹp nhanh hơn khi bạn cần thực hiện một hành động không thể thực hiện được bằng toán tử ngôn ngữ tiêu chuẩn.

Theo tôi, về khả năng bảo trì, khả năng mở rộng và khả năng đọc, ngôn ngữ Go chiếm ưu thế, mặc dù nó thua về tính dài dòng.

Lập trình tổng quát trong một số trường hợp mang lại cho chúng ta những lợi ích không thể phủ nhận. Điều này được minh họa rõ ràng bằng gói sắp xếp. Vì vậy, để sắp xếp bất kỳ danh sách nào, chúng ta chỉ cần triển khai giao diện Sort.Interface.

import "sort"

type Names []string

func (ns Names) Len() int {
    return len(ns)
}

func (ns Names) Less(i, j int) bool {
    return ns[i] < ns[j]
}

func (ns Names) Swap(i, j int) {
    ns[i], ns[j] = ns[j], ns[i]
}

func main() {
    names := Names{"London", "Berlin", "Rim"}
    sort.Sort(names)
}

Nếu bạn thực hiện bất kỳ dự án nguồn mở nào và chạy lệnh grep “interface{}” -R, bạn sẽ thấy tần suất sử dụng các giao diện khó hiểu. Những đồng chí thân cận sẽ nói ngay rằng tất cả những điều này là do thiếu thuốc generic. Tuy nhiên, đây không phải là luôn luôn như vậy. Hãy lấy DELPHI làm ví dụ. Mặc dù có sự hiện diện của các dạng tổng quát này, nó vẫn chứa một loại VARIANT đặc biệt dành cho các hoạt động với các kiểu dữ liệu tùy ý. Ngôn ngữ Go cũng làm như vậy.

Từ khẩu đại bác đến chim sẻ

Và chiếc áo bó phải vừa với kích cỡ của điên cuồng. Stanislav Lec.

Nhiều người hâm mộ cực đoan có thể khẳng định rằng cờ vây có một cơ chế khác để tạo ra những cái chung - sự phản ánh. Và họ sẽ đúng... nhưng chỉ trong những trường hợp hiếm hoi.

Rob Pike cảnh báo chúng tôi:

Đây là một công cụ mạnh mẽ nên được sử dụng một cách thận trọng. Nên tránh trừ khi thực sự cần thiết.

Wikipedia cho chúng ta biết những điều sau:

Phản ánh đề cập đến quá trình trong đó một chương trình có thể giám sát và sửa đổi cấu trúc và hành vi của chính nó trong quá trình thực thi. Mô hình lập trình làm cơ sở cho sự phản chiếu được gọi là lập trình phản chiếu. Đây là một loại siêu lập trình.

Tuy nhiên, như bạn biết, bạn phải trả tiền cho mọi thứ. Trong trường hợp này là:

  • khó khăn khi viết chương trình
  • tốc độ thực hiện chương trình

Vì vậy, sự phản xạ phải được sử dụng một cách thận trọng, giống như một loại vũ khí cỡ nòng lớn. Việc sử dụng sự phản ánh một cách thiếu suy nghĩ sẽ dẫn đến các chương trình không thể đọc được, liên tục xảy ra lỗi và tốc độ thấp. Điều duy nhất đối với một lập trình viên hợm hĩnh là có thể khoe mã của mình trước những đồng nghiệp khác, thực dụng và khiêm tốn hơn.

Hành trang văn hóa từ Tập? Không, từ một số ngôn ngữ!

Cùng với tài sản, các khoản nợ cũng được để lại cho những người thừa kế.

Mặc dù thực tế là nhiều người tin rằng ngôn ngữ này hoàn toàn dựa trên di sản C, nhưng thực tế không phải vậy. Ngôn ngữ kết hợp nhiều khía cạnh của các ngôn ngữ lập trình tốt nhất.

cú pháp

Trước hết, cú pháp của các cấu trúc ngữ pháp dựa trên cú pháp của ngôn ngữ C. Tuy nhiên, ngôn ngữ DELPHI cũng có ảnh hưởng đáng kể. Như vậy, chúng ta thấy rằng các dấu ngoặc đơn dư thừa làm giảm đáng kể khả năng đọc của chương trình đã bị loại bỏ hoàn toàn. Ngôn ngữ này cũng chứa toán tử “:=” vốn có của ngôn ngữ DELPHI. Khái niệm gói được mượn từ các ngôn ngữ như ADA. Việc khai báo các thực thể không được sử dụng được mượn từ ngôn ngữ PROLOG.

Ngữ nghĩa

Các gói được dựa trên ngữ nghĩa của ngôn ngữ DELPHI. Mỗi gói đóng gói dữ liệu và mã và chứa các thực thể riêng tư và công cộng. Điều này cho phép bạn giảm giao diện gói xuống mức tối thiểu.

Hoạt động triển khai theo phương pháp ủy quyền được mượn từ ngôn ngữ DELPHI.

Tổng hợp

Không phải vô cớ mà có một câu nói đùa: Go được phát triển trong khi chương trình C đang được biên dịch. Một trong những điểm mạnh của ngôn ngữ này là khả năng biên dịch cực nhanh. Ý tưởng này được mượn từ ngôn ngữ DELPHI. Mỗi gói Go tương ứng với một mô-đun DELPHI. Các gói này chỉ được biên dịch lại khi thực sự cần thiết. Do đó, sau lần chỉnh sửa tiếp theo, bạn không cần phải biên dịch toàn bộ chương trình mà chỉ biên dịch lại các gói đã thay đổi và các gói phụ thuộc vào các gói đã thay đổi này (và thậm chí sau đó, chỉ khi giao diện gói đã thay đổi).

Cấu trúc cấp cao

Ngôn ngữ chứa nhiều cấu trúc cấp cao khác nhau không liên quan gì đến các ngôn ngữ cấp thấp như C.

  • Dây
  • Bảng băm
  • lát
  • Kiểu gõ vịt được mượn từ các ngôn ngữ như RUBY (rất tiếc là nhiều ngôn ngữ không hiểu hoặc không sử dụng hết tiềm năng của nó).

Quản lý bộ nhớ

Quản lý bộ nhớ nói chung xứng đáng có một bài viết riêng. Nếu trong các ngôn ngữ như C++, việc kiểm soát hoàn toàn được giao cho nhà phát triển thì ở các ngôn ngữ sau này như DELPHI, mô hình đếm tham chiếu đã được sử dụng. Với cách tiếp cận này, các tham chiếu theo chu kỳ không được phép, vì các cụm mồ côi đã được hình thành nên Go đã tích hợp tính năng phát hiện các cụm đó (như C#). Ngoài ra, trình thu gom rác hiệu quả hơn hầu hết các triển khai được biết đến hiện nay và có thể được sử dụng cho nhiều tác vụ thời gian thực. Bản thân ngôn ngữ này nhận ra các tình huống khi một giá trị để lưu trữ một biến có thể được phân bổ trên ngăn xếp. Điều này làm giảm tải cho trình quản lý bộ nhớ và tăng tốc độ của chương trình.

Đồng thời và đồng thời

Tính song song và tính cạnh tranh của ngôn ngữ là điều không thể khen ngợi. Không có ngôn ngữ cấp thấp nào thậm chí có thể cạnh tranh từ xa với Go. Công bằng mà nói, điều đáng chú ý là mô hình này không phải do các tác giả của ngôn ngữ này phát minh ra mà chỉ đơn giản được mượn từ ngôn ngữ ADA cũ. Ngôn ngữ này có khả năng xử lý hàng triệu kết nối song song bằng cách sử dụng tất cả các CPU, đồng thời giải quyết các vấn đề ít phức tạp hơn với tình trạng bế tắc và điều kiện chạy đua điển hình cho mã đa luồng.

Lợi ích kèm theo

Nếu nó có lợi nhuận, mọi người sẽ trở nên vị tha.

Ngôn ngữ cũng cung cấp cho chúng ta một số lợi ích chắc chắn:

  • Một tệp thực thi duy nhất sau khi xây dựng dự án giúp đơn giản hóa đáng kể việc triển khai ứng dụng.
  • Gõ tĩnh và suy luận kiểu có thể giảm đáng kể số lỗi trong mã của bạn, ngay cả khi không viết bài kiểm tra. Tôi biết một số lập trình viên không viết bài kiểm tra nào cả và chất lượng mã của họ không bị ảnh hưởng đáng kể.
  • Biên dịch chéo rất đơn giản và tính di động tuyệt vời của thư viện tiêu chuẩn, giúp đơn giản hóa đáng kể việc phát triển các ứng dụng đa nền tảng.
  • Biểu thức chính quy RE2 an toàn theo luồng và có thời gian thực hiện có thể dự đoán được.
  • Một thư viện tiêu chuẩn mạnh mẽ cho phép hầu hết các dự án thực hiện mà không cần khuôn khổ của bên thứ ba.
  • Ngôn ngữ này đủ mạnh để tập trung vào vấn đề hơn là cách giải quyết nó, nhưng cũng đủ ở mức độ thấp để vấn đề có thể được giải quyết một cách hiệu quả.
  • Hệ sinh thái Go đã có sẵn các công cụ được phát triển sẵn dùng cho mọi trường hợp: kiểm tra, tài liệu, quản lý gói, linters mạnh mẽ, tạo mã, phát hiện điều kiện chạy đua, v.v.
  • Phiên bản Go 1.11 đã giới thiệu tính năng quản lý phụ thuộc ngữ nghĩa tích hợp sẵn, được xây dựng dựa trên dịch vụ lưu trữ VCS phổ biến. Tất cả các công cụ tạo nên hệ sinh thái Go đều sử dụng các dịch vụ này để tải xuống, xây dựng và cài đặt mã từ chúng chỉ trong một lần. Và điều đó thật tuyệt. Với sự xuất hiện của phiên bản 1.11, vấn đề về phiên bản gói cũng đã được giải quyết hoàn toàn.
  • Bởi vì ý tưởng cốt lõi của ngôn ngữ là giảm thiểu ma thuật nên ngôn ngữ này khuyến khích các nhà phát triển xử lý lỗi một cách rõ ràng. Và điều này đúng, vì nếu không, nó sẽ hoàn toàn quên mất việc xử lý lỗi. Một điều nữa là hầu hết các nhà phát triển đều cố tình bỏ qua việc xử lý lỗi, thay vì xử lý chúng chỉ chuyển tiếp lỗi lên trên.
  • Ngôn ngữ này không triển khai phương pháp OOP cổ điển, vì ở dạng thuần túy, Go không có ảo hóa. Tuy nhiên, đây không phải là vấn đề khi sử dụng giao diện. Sự vắng mặt của OOP làm giảm đáng kể rào cản gia nhập đối với người mới bắt đầu.

Đơn giản vì lợi ích cộng đồng

Phức tạp thì dễ, đơn giản hóa thì khó.

Cờ vây được thiết kế đơn giản và nó đã đạt được mục tiêu đó. Nó được viết cho những lập trình viên thông minh, những người hiểu được lợi ích của việc làm việc nhóm và cảm thấy mệt mỏi với sự biến đổi vô tận của các ngôn ngữ cấp Doanh nghiệp. Sở hữu một tập hợp cấu trúc cú pháp tương đối nhỏ trong kho vũ khí của mình, nó thực tế không bị thay đổi theo thời gian, vì vậy các nhà phát triển có rất nhiều thời gian rảnh rỗi để phát triển chứ không phải dành cho việc nghiên cứu không ngừng những đổi mới về ngôn ngữ.

Các công ty cũng nhận được một số lợi thế: rào cản gia nhập thấp cho phép họ nhanh chóng tìm được chuyên gia và tính bất biến của ngôn ngữ cho phép họ sử dụng cùng một mã ngay cả sau 10 năm.

Kết luận

Kích thước bộ não lớn chưa bao giờ khiến con voi nào đoạt giải Nobel.

Đối với những lập trình viên đặt cái tôi cá nhân lên trên tinh thần đồng đội, cũng như những nhà lý thuyết yêu thích thử thách học tập và "tự hoàn thiện" không ngừng, ngôn ngữ này thực sự tệ, vì đây là ngôn ngữ thủ công có mục đích chung không cho phép bạn tiếp cận. niềm vui thẩm mỹ từ kết quả công việc của bạn và thể hiện mình là người chuyên nghiệp trước mặt đồng nghiệp (với điều kiện là chúng tôi đo lường trí thông minh theo các tiêu chí này chứ không phải bằng IQ). Giống như mọi thứ trong cuộc sống, đó là vấn đề ưu tiên cá nhân. Giống như tất cả những đổi mới đáng giá, ngôn ngữ này đã phải trải qua một chặng đường dài từ sự phủ nhận phổ quát đến sự chấp nhận rộng rãi. Ngôn ngữ khéo léo ở sự đơn giản của nó, và như bạn biết, mọi thứ khéo léo đều đơn giản!

Nguồn: www.habr.com

Thêm một lời nhận xét