1 คะแนน โดย GN⁺ 2 시간 전 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • Go เป็นตัวเลือกที่ช่วยลดความซับซ้อนเกินจำเป็นของการพัฒนาแบ็กเอนด์ โดยมีจุดเด่นหลักคือ คอมไพล์เร็ว การ ดีพลอยเป็นไบนารีไฟล์เดียว และการจัดการ dependency ที่เสถียร
  • แทนที่จะใช้ abstraction ซับซ้อนอย่าง decorator, metaclass, macro, trait หรือ monad นั้น Go เลือกการออกแบบภาษาที่เรียบง่าย โดยยึด struct, ฟังก์ชัน, interface, goroutine และ channel เป็นแกนหลัก
  • ด้วย standard library และเครื่องมือพื้นฐานอย่าง embed, html/template, net/http, database/sql, encoding/json, go test, pprof ก็สามารถจัดการได้ตั้งแต่เว็บแอป ฐานข้อมูล การทดสอบ benchmark ไปจนถึง profiling
  • goroutine เป็นหน่วยรันแบบ stackful ที่มีต้นทุนประมาณ 2KB และสามารถจัดการงานพร้อมกันกับการส่งต่อการยกเลิกได้อย่างเรียบง่ายผ่าน channel, sync.Mutex, race detector และ context.Context
  • ลำดับงานตั้งแต่ go mod init, go build, scp, systemctl restart ชี้ให้เห็นว่าแนวทางดีพลอยแบบเรียบง่ายด้วย Go binary หนึ่งไฟล์และ Postgres นั้นเหมาะกว่าการมี node_modules, การตั้งค่า Docker·Kubernetes ที่ซับซ้อน หรือ microservices ที่มากเกินไป

เหตุผลที่ควรเลือก Go

  • Go เป็นตัวเลือกที่ช่วยลดความซับซ้อนเกินจำเป็นของการพัฒนาแบ็กเอนด์ โดยมีจุดเด่นหลักคือ คอมไพล์เร็ว ดีพลอยเป็นไบนารีไฟล์เดียว และการจัดการ dependency ที่เสถียร
  • เช่นเดียวกับที่ HTML ยังคงเป็นทางเลือกแทนความซับซ้อนเกินจำเป็นในฝั่งฟรอนต์เอนด์ Go ก็เป็นตัวเลือกเพื่อทำให้แบ็กเอนด์เรียบง่ายมานานกว่าสิบปีแล้ว
  • ถ้าแค่ทำฟอร์มง่ายๆ หรือแอป CRUD ที่รับคำขอราว 40 ครั้งต่อวินาที การระดมทั้งแพ็กเกจ Node จำนวนมาก เครื่องมือ build ของ TypeScript, Kubernetes, ทีมแพลตฟอร์ม Rails หรือถึงขั้นเขียนใหม่ด้วย Rust ก็ถือว่าเกินไป
  • เป้าหมายของ Go ไม่ใช่ “abstraction อันชาญฉลาด” แต่คือ โค้ดที่อ่านง่าย ผลลัพธ์ที่พร้อมดีพลอย และภาระในการดูแลระบบที่ต่ำ

การออกแบบภาษาที่ตั้งใจให้เรียบจนน่าเบื่อ

  • ที่ Go ดูน่าเบื่อก็เพราะมันถูกออกแบบมาแบบนั้น โดยไม่ได้มี abstraction ซับซ้อนอย่าง decorator, metaclass, macro, trait หรือ monad
  • องค์ประกอบหลักมีอยู่แค่ struct, ฟังก์ชัน, interface, goroutine และ channel เท่านั้น
  • เป้าหมายคือความเรียบง่ายระดับที่อ่านสเปกได้ในเวลาไม่นาน และเริ่มเขียนโค้ดอย่างมีประสิทธิภาพได้ภายในวันเดียวกัน
  • ความน่าเบื่อนี้กลับเป็นข้อดีใน codebase ของทีม
    • แม้แต่นักพัฒนาจูเนียร์ที่เพิ่งเข้ามาเมื่อเดือนก่อน ก็ยังอ่านโค้ดที่ principal เขียนไว้เมื่อ 2 ปีก่อนได้
    • gofmt บังคับรูปแบบเดียว ทำให้การถกเถียงเรื่องสไตล์โค้ดลดลง
    • ตัวภาษาเองทำให้ยัด abstraction ที่ซับซ้อนเกินไปลงใน codebase ได้ยาก

standard library ทำหน้าที่เหมือน framework

  • Go สามารถสร้างเว็บแอปได้ด้วย standard library เพียงอย่างเดียว โดยไม่ต้องมีเว็บ framework แยก
  • ใช้ embed, html/template, net/http ก็สร้างแอปที่ฝัง HTML template ไว้ในไบนารีและ render ผ่าน HTTP handler ได้
package main

import (
    "embed"
    "html/template"
    "net/http"
)

//go:embed templates/*.html
var files embed.FS

var tmpl = template.Must(template.ParseFS(files, "templates/*.html"))

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        tmpl.ExecuteTemplate(w, "index.html", map[string]string{
            "Name": "asshole",
        })
    })

    http.ListenAndServe(":8080", nil)
}
  • ตัวอย่างนี้คือเว็บแอปที่ใช้งานได้จริง และ HTML template ถูกคอมไพล์ฝังเข้าไปในไบนารี
  • ไม่ต้องมี webpack, Vite, dev server หรือ node_modules ก้อนใหญ่ แค่ go build แล้วดีพลอยไฟล์เดียวได้เลย
  • standard library และเครื่องมือพื้นฐานก็เพียงพอสำหรับงานแบ็กเอนด์หลักๆ
    • ฐานข้อมูล: database/sql
    • JSON: encoding/json
    • เรียกใช้บริการอื่น: net/http client
    • การทำงานพร้อมกัน: คีย์เวิร์ด go
    • การทดสอบ: go test
    • benchmark: go test -bench
    • profiling: pprof

โครงสร้าง standard library ที่ลึกและใช้งานต่อเนื่องได้

  • io.Reader และ io.Writer

    • io.Reader และ io.Writer เป็น interface ที่มีเมธอดเดียวคนละตัว แต่ทำหน้าที่เป็นรากฐานสำคัญทั่วทั้ง ecosystem ของ Go
    • คุณสามารถต่อ body ของ HTTP response เข้ากับ gzip writer แล้วต่อไปยังไฟล์บนดิสก์ได้ด้วยโค้ดเพียงเล็กน้อย
    • เพราะแพ็กเกจหลักจำนวนมากใช้สอง interface นี้ร่วมกัน จึงนำรูปแบบเดิมไปใช้ซ้ำได้ในหลายจุด
  • context.Context

    • context.Context คือวิธีมาตรฐานสำหรับการส่งต่อการยกเลิก
    • เมื่อผู้ใช้ปิดแท็บเบราว์เซอร์ request context จะถูกยกเลิก และต่อเนื่องไปถึง query ฐานข้อมูลหรือ HTTP call ไปยังบริการย่อยได้
    • ถ้าไม่อยากให้ goroutine รั่ว หรือมี query ซอมบี้ค้างกิน connection pool ก็ควรส่ง context เป็นอาร์กิวเมนต์ตัวแรกและปฏิบัติตามมัน
  • แพ็กเกจ encoding

    • encoding/json, encoding/xml, encoding/csv, encoding/binary ทั้งหมดรวมอยู่ใน standard library
    • รูปแบบการใช้งานที่คล้ายกัน ทั้ง struct tag และการ decode ด้วย pointer ทำให้เรียนรู้อันหนึ่งแล้วใช้อีกอันได้ไม่ยาก

โมเดล concurrency ที่ช่วยลดความเจ็บปวด

  • goroutine ไม่ใช่ OS thread โดยตรง แต่เป็นหน่วยรันแบบ stackful ที่ runtime multiplex อยู่บน OS thread
  • goroutine มีต้นทุนเริ่มต้นราว 2KB และสามารถสร้างได้ถึง 100,000 ตัวแม้บนโน้ตบุ๊ก
  • channel ทำหน้าที่เป็นท่อแบบมีชนิดข้อมูลระหว่าง goroutine เมื่อฝั่งหนึ่งส่งและอีกฝั่งรับ runtime จะจัดการ synchronization ให้
  • หากต้องใช้ shared state ก็ใช้ sync.Mutex ได้ และ race detector จะช่วยหาปัญหา data race
  • แม้แต่ตัวดึงข้อมูล HTTP แบบขนานก็เขียนได้โดยไม่ต้องมีไลบรารีหรือ framework เพิ่ม และไม่ต้องยึดติดพิธีกรรมแบบ async/await
results := make(chan string, len(urls))
for _, url := range urls {
    go func(u string) {
        resp, _ := http.Get(u)
        results <- resp.Status
    }(url)
}
for range urls {
    fmt.Println(<-results)
}

ตัวอย่าง route CRUD ที่ใช้งานจริง

  • แม้แต่ route แนว CRUD ที่อ่านโพสต์จาก Postgres แล้ว render HTML ก็ยังเขียนได้เรียบง่ายพอจะอยู่ในหน้าจอเดียว
//go:embed templates/*.html
var tmplFS embed.FS

var tmpl = template.Must(template.ParseFS(tmplFS, "templates/*.html"))

type Post struct {
    ID    int
    Title string
    Body  string
}

func postsHandler(db *sql.DB) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        rows, err := db.QueryContext(r.Context(),
            "SELECT id, title, body FROM posts ORDER BY id DESC LIMIT 50")
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
        defer rows.Close()

        var posts []Post
        for rows.Next() {
            var p Post
            if err := rows.Scan(&p.ID, &p.Title, &p.Body); err != nil {
                http.Error(w, err.Error(), http.StatusInternalServerError)
                return
            }
            posts = append(posts, p)
        }

        tmpl.ExecuteTemplate(w, "posts.html", posts)
    }
}
  • ตัวอย่างนี้แสดงฐานข้อมูล template และ HTTP handler ไว้ในที่เดียว
  • เพราะ r.Context() ถูกส่งต่อไปยัง SQL query หากการเชื่อมต่อถูกปิด query ก็สามารถถูกยกเลิกได้ด้วย
  • ไม่ต้องมี ORM, DI container, service layer หรือไดเรกทอรี controllers/ ที่เต็มไปด้วย abstract base class ก็ยังอ่านจากบนลงล่างแล้วเข้าใจการทำงานได้

การจัดการ dependency ที่ไม่ทำลายวันหยุดสุดสัปดาห์

  • เริ่มโมดูลด้วย go mod init แล้ว dependency จะถูกบันทึกไว้ใน go.mod และ go.sum
  • go.sum คือ บันทึกเชิงเข้ารหัส ของสิ่งที่ดาวน์โหลดมาจริง ทำให้ตรวจสอบได้ว่ามี dependency ที่ต่างจากที่คาดไว้หลุดเข้ามาหรือไม่
  • ไม่มีความซับซ้อนแบบไดเรกทอรี node_modules, lockfile drift ระหว่างเครื่องพัฒนากับ CI, peer dependencies, optional dependencies, devDependencies หรือ peerDependenciesMeta
  • ถ้าต้องการ build แบบออฟไลน์ go mod vendor จะดาวน์โหลด dependency ลงในไดเรกทอรี vendor/ และ toolchain จะใช้ให้โดยอัตโนมัติ
  • การรวมทั้งโปรเจกต์และ dependency ลงใน tarball ไฟล์เดียวก็ทำได้ จึงได้เปรียบทั้งด้านปฏิบัติการและการตรวจสอบความปลอดภัย

เครื่องมือที่มากับ compiler

  • เครื่องมือพื้นฐานของ Go มาพร้อมใช้งาน โดยไม่ต้องอาศัยปลั๊กอินภายนอกหรือไฟล์ตั้งค่าแยก
  • gofmt ทำให้รูปแบบโค้ดเป็นมาตรฐานเดียวกัน และลดทั้งการถกเถียงเรื่อง format กับ diff ที่บวมเพราะช่องว่าง
  • go vet ใช้จับความผิดพลาดที่ชัดเจน
  • go test ใช้รันทดสอบ
  • go test -race ใช้รันทดสอบพร้อม race detector เพื่อหาปัญหา data race
  • go test -bench ใช้รัน benchmark
  • go test -cover ใช้ดู test coverage
  • go tool pprof สามารถดึง flame graph ของการใช้ CPU และหน่วยความจำผ่าน HTTP endpoint ของ service production ที่กำลังรันอยู่ได้

การดีพลอยจบได้ด้วยคำสั่งคัดลอก

  • แกนหลักของการดีพลอย Go คือ build ไบนารี คัดลอกขึ้นเซิร์ฟเวอร์ แล้วรันมัน
GOOS=linux GOARCH=amd64 go build -o myapp ./cmd/myapp
scp myapp user@server:/usr/local/bin/
ssh user@server 'systemctl restart myapp'
  • ขั้นตอนนี้ทำให้ดีพลอยได้โดยไม่ต้องมี Dockerfile, multi-stage build, การแจ้งเตือน CVE ของ base image, Kubernetes manifest, Helm chart, ArgoCD, service mesh หรือ sidecar
  • แค่ไบนารี static link ขนาดประมาณ 12MB และไฟล์ systemd unit ราว 20 บรรทัด ก็เพียงพอสำหรับ production
  • ถ้าจำเป็นต้องใช้ Docker จริงๆ การเอา Go binary ไปใส่ในอิมเมจ FROM scratch ก็เพียงพอแล้ว

เมื่อนำไปเทียบกับ framework

  • framework อย่าง Rails, Django, Express, Next.js ต่างก็มาพร้อมภาระของตัวเอง เช่น ขั้นตอนดีพลอย, ORM, admin, middleware, คำเตือน npm หรือการเปลี่ยนแปลงธรรมเนียมการทำ routing
  • Go binary ถูกคอมไพล์แล้วรันได้เลย และจุดแข็งคือความเสถียรที่อาจยังรันได้แม้เวลาผ่านไปอีก 5 ปี
  • เมื่อเทียบกับโลกที่ framework อาจถูกเลิกใช้เร็วกว่า หรือผู้ดูแลเกิดหมดไฟ โมเดลการรันที่เรียบง่ายของ Go จึงโดดเด่นขึ้นมา

Go binary เดียว ดีกว่า microservices

  • microservices ไม่ควรถูกตั้งให้เป็นตัวเลือกเริ่มต้น ควรเริ่มจาก monolith ก่อน
  • โครงสร้างที่แนะนำคือ Go binary หนึ่งไฟล์, Postgres หนึ่งตัว และมี Redis หนึ่งตัวเฉพาะเมื่อจำเป็นจริงๆ
  • คุณสามารถเสิร์ฟทั้ง HTML และ JSON API บนพอร์ตเดียวกัน และรันทั้งหมดบน VPS เครื่องเดียวได้
  • เพราะ Go มีต้นทุน goroutine ต่ำและจัดการ concurrency ได้ดี จึงขยายไปถึง 10,000 requests ต่อวินาทีได้โดยไม่ยาก
  • หากวันหนึ่งจำเป็นต้องแยกจริง ก็สามารถย้ายแพ็กเกจออกจาก Go monolith ไปยังรีโปแยกได้
  • เพราะมี interface อยู่แล้ว ภาษาเองจึงช่วยผลักโครงสร้างให้พร้อมต่อการแยกส่วนอย่างเป็นธรรมชาติ

generic และการจัดการ error

  • if err != nil ไม่ใช่บั๊ก แต่มันคือฟีเจอร์
  • มันบังคับให้ตัดสินใจด้วยตัวเองในแต่ละจุดที่ล้มเหลว แทนที่จะซ่อน error ไว้
  • การซ้อน try/catch ไม่ได้ทำให้ error หายไป แค่อาจซ่อนมันไว้จนถึงเวลาที่ production พัง
  • generic ถูกเพิ่มเข้ามาใน Go 1.18 และใช้เมื่อจำเป็นก็พอ

บทสรุป

  • ไม่จำเป็นเสมอไปว่าต้องมี framework, microservices, การเขียนใหม่ด้วย Rust หรือ JavaScript meta-framework ตัวใหม่
  • ลองรัน go mod init, เขียน main.go, ฝัง template ด้วย embed แล้วคอมไพล์เพื่อนำไปดีพลอยตามขั้นตอนเรียบง่ายนั้น
  • ตัวเลือกที่น่าเบื่ออาจเป็นตัวเลือกที่ถูกต้อง และ Go ก็คือตัวเลือกนั้น

1 ความคิดเห็น

 
GN⁺ 2 시간 전
ความเห็นจาก Lobste.rs
  • ไม่ได้จะโทษคนส่งสารนะ แต่สำนวนแบบบล็อกแนวนี้มันทั้งน่าเหนื่อยและดูงี่เง่า ตอนแรกอาจขำอยู่หรอก แต่พอทำซ้ำไปเรื่อย ๆ ระดับความรำคาญก็พุ่งแบบทวีคูณ
    ถึงอย่างนั้น Go ก็ยังดีอยู่ดี ช่วงนี้เพิ่งย้ายจากโปรเจ็กต์ TypeScript ไปทำโปรเจ็กต์ Go แล้วสุขภาพจิตกับขวัญกำลังใจในการทำงานดีขึ้นอย่างรวดเร็ว
    ผมยอมรับได้กับคำพูดที่ว่า if err != nil ไม่ใช่บั๊กแต่เป็นฟีเจอร์ แต่ก็ยังมองว่านี่คือข้อบกพร่องใหญ่ที่สุดของ Go ถ้ามี sum types ก็น่าจะทำให้ใช้งานได้สบายกว่านี้มากโดยไม่ต้องพึ่งการยืนยันชนิดตอนรันไทม์

    • ผมว่ามันยังดีกว่าบทความ AI ไร้แก่นที่พยายามวางตัวก้ำกึ่งกับทุกอย่างแต่ไม่มีจุดยืนจริง ๆ
    • อ่านแล้วก็สนุกดี แต่ไม่ค่อยได้เห็นบทความแนวนี้บ่อยนัก ถึงอย่างนั้นเรียกคนว่า “walnut” ก็ยังตลกและน่ารักกว่า “dipshit”
      ถ้าจะเขียนแนวนี้ อย่างน้อยก็ควรด่ากันให้มีไหวพริบหน่อย
    • เห็นด้วย มีวิธีรายงานไหม? มันไม่เข้ากับหมวดการรายงานไหนเลย
  • พออ่านคอมเมนต์อื่น ๆ แล้วคงเป็นความเห็นที่ไม่ค่อยนิยม และผมก็ไม่อยากให้ฟังดูห้วนเกินไป แต่ผมเกลียด Go จริง ๆ
    Go คือภาษาที่เอาไวยากรณ์ที่ก็พอใช้ได้ไปวางบนรันไทม์ที่มีประสิทธิภาพด้าน concurrency แล้วใช้อำนาจของ Google ดัน ecosystem ขึ้นมา นอกนั้นผมว่ามันแย่มาก
    ปัญหาใหญ่ที่สุดคือมันเหมือนถูกออกแบบมาให้จงใจเมินทั้งงานวิจัยด้านการออกแบบภาษาโปรแกรมหลายสิบปี หรือแม้แต่แนวปฏิบัติจริงในการทำงาน กว่าจะมี generics ก็รอตั้งหลายสิบปี
    ไม่ได้หมายความว่าทุกอย่างต้องใช้ dependent types เสมอไป แต่มันก็มีระดับที่พอดีอยู่ Go แทบไม่มีความสามารถด้านการโมเดลข้อมูล การโมเดลข้อกำหนดคงที่ และการจัดโครงสร้างโค้ดที่ภาษาสมัยใหม่ควรมี Rust แม้เส้นโค้งการเรียนรู้จะชันกว่า แต่ในแง่นี้ดีกว่ามาก และจริง ๆ แล้วก็ไม่จำเป็นต้องมีระบบชนิดที่ประณีตเท่า Rust ก็ยังดีได้ ถ้ากังวลเรื่องเวลาคอมไพล์ ก็ยังสร้างระบบชนิดที่เรียบง่ายแต่ใช้งานได้จริง เร็ว มีพลังในการอธิบาย และสมเหตุสมผลได้
    แล้ว if err != nil ก็เป็นวิธีที่แย่ที่สุดในการทำให้โค้ดเต็มไปด้วย noise จากการจัดการข้อผิดพลาด ผมไม่เข้าใจจริง ๆ ว่าฝั่ง Go แพ้ทาง sum types อะไรนักหนา ในจุดนี้แม้แต่ exception ของ Java ยังดีกว่า ความจริงคือภาษาไม่มีความสามารถในการจัดการข้อผิดพลาดที่ดีกว่านี้ คนเลยเข้าใจชิ้นงานอุดรอยรั่วที่แย่ที่สุดว่าเป็นฟีเจอร์
    ถ้าต้นฉบับไม่ได้ทำตัวอวดดีตั้งแต่แรก ผมก็คงไม่เขียนคอมเมนต์แบบนี้ “ก็ใช้ X ไปสิ” เป็นคำพูดโง่ ๆ ใช้เครื่องมือที่เหมาะกับกรณีใช้งาน ที่สบายมือ และทำงานได้มีประสิทธิผลก็พอ ถ้านั่นคือ Go ก็ใช้ Go ถ้าไม่ใช่ก็เลือกอย่างอื่น

    • ผมว่า Go ยืนอยู่ในพื้นที่การออกแบบที่ให้ความสำคัญกับความเรียบง่ายสำหรับนักพัฒนามือใหม่ที่ทำงานกับ codebase และองค์กรขนาดใหญ่ มากกว่าสิ่งอื่นแทบทั้งหมด เพราะงั้นแม้นักพัฒนาประสบการณ์น้อยก็ยังอ่านโค้ดและแก้เฉพาะจุดได้ง่ายโดยไม่ต้องสะสมบริบทมากนัก
      โดยเฉพาะในองค์กรอย่าง Google ที่มีนักพัฒนาหลายพันคน และระยะเวลาที่อยู่กับทีมหรือบริษัทใดบริษัทหนึ่งอาจสั้น แบบนี้ก็ช่วยได้มาก
      ในบริบทนี้ โดยเฉพาะสำหรับนักพัฒนาที่ยังไม่ชำนาญ การไม่มีระบบชนิดขั้นสูงก็กลายเป็นข้อดีในระดับหนึ่ง เพราะแทบไม่ต้องคิดเรื่อง type เกินกว่าพื้นฐานอย่าง basic types หรือ struct ภาษาแทบไม่ให้เครื่องมือสำหรับโมเดลข้อมูล แต่ในอีกมุมก็เลยเขียนโค้ดจำนวนมากได้โดยไม่ต้องคิดมาก
      ผมว่าไม่ค่อยดีต่อความถูกต้องแม่นยำในระดับภาษาเท่าไร แต่ในองค์กรใหญ่ก็มักพึ่งพาโครงสร้างพื้นฐานรอบข้างอย่างการวิเคราะห์ monorepo, CI/CD, canary testing และเครื่องมือสังเกตการณ์มากกว่า ซึ่งรับภาระได้มากกว่าองค์กรเล็กอย่างชัดเจน
      ตัวผมเองก็ชอบ Go อยู่พอสมควรด้วยเหตุผลคล้ายกันคือภาระทางความคิดต่ำ เพราะแค่เขียนโค้ดให้บางโปรเจ็กต์เป็นครั้งคราว และไม่ได้มีส่วนลึกกับโปรเจ็กต์ระยะยาวทุกวัน การที่ผมเข้าไปใน codebase ที่ไม่ได้แตะมาหนึ่งเดือนแล้วทำงานให้เสร็จในเวลาไม่ถึงชั่วโมงได้นี่เป็นข้อดีมาก แต่ถ้าต้องเป็นนักพัฒนาเต็มเวลาของโปรเจ็กต์ซับซ้อน ผมคงชอบมันน้อยกว่านี้
    • Dart ก็เป็นภาษาอีกตัวของ Google ที่ดูไม่ได้เมินงานวิจัยหลายสิบปีเหมือนกัน แต่ก็ไม่มีใครใช้ข้างนอก Flutter ส่วน Go ก็โอเคดี
    • บทความนี้ลอกฟอร์แมตมีมที่ชวนหาเรื่องและทำตัวเหนือกว่าอยู่แล้ว เพราะงั้นมันย่อมไปกระตุ้นคนแน่ ๆ และเพราะผมคิดว่าประเด็นของบทความควรค่าแก่การคุยกันจริงจัง ไม่ใช่โยนให้กลายเป็นสงครามคอมเมนต์ ผมเลยไม่ค่อยชอบมัน
      ผมว่าคนทำ Go เลือกโฟกัสกับการทำพื้นฐานให้แน่น เพราะภาษาที่ผ่านมาและชุมชนวิจัยทฤษฎีภาษาโปรแกรมก่อนหน้านี้มักละเลยพื้นฐานพวกนี้ คนชอบหมกมุ่นกับระบบชนิดที่ครอบคลุมที่สุด แต่ยิ่งระบบชนิดซับซ้อนและแสดงความหมายได้มาก ผลตอบแทนก็ยิ่งลดลง และไม่ว่าคุณจะทุ่มกับระบบชนิดมากแค่ไหน มันก็ชดเชยการจัดการแพ็กเกจที่เลวร้าย เครื่องมือ build ที่บังคับให้ทีมต้องเรียน DSL ใหม่ ระบบเอกสารที่ไม่สร้างข้อมูลชนิดหรือลิงก์ไปเอกสารแพ็กเกจภายนอกให้อัตโนมัติ standard library ที่อ่อนแอ ปัญหาด้านประสิทธิภาพร้ายแรง การไม่มีแนวทาง static compilation เวลาบิลด์อันทรมาน เส้นโค้งการเรียนรู้ที่ชัน ระบบชนิดที่ลงโทษผู้ใช้ ไวยากรณ์ที่อ่านยาก หรือการรวมเข้ากับ editor ที่ห่วยแตกได้
      การบอกว่า Go ไม่มีฟีเจอร์สำหรับโมเดลข้อมูลเลยนั้นผิดชัดเจน ในทุกภาษาคุณก็โมเดลข้อมูลและข้อกำหนดคงที่ได้ทั้งนั้น และ Go ก็มีระบบชนิดที่พอจะบังคับโมเดลเหล่านั้นได้พอสมควร
      Rust ยอดเยี่ยม และเป็นตัวเลือกที่ดีถ้าความเร็วรอบการทำซ้ำไม่สำคัญ หรือถ้าต้อง deploy ลง bare metal หรือถ้ามีความต้องการด้านความถูกต้องและประสิทธิภาพสูงมาก แต่ไม่ใช่ค่าเริ่มต้นที่ดีสำหรับการพัฒนาแอปพลิเคชันทั่วไป โดยเฉพาะการพัฒนาเป็นทีม ต้องพิมพ์ if err != nil เยอะก็จริง แต่ผมไม่คิดว่ามีใครที่คอขวดอยู่ที่จำนวนครั้งการกดแป้นพิมพ์ต่อวินาที
    • นอกจาก Rust แล้วแทบไม่มีภาษาสมัยใหม่ไหนมีฟีเจอร์แบบนี้ ถ้าอยากเขียนด้วย Gleam หรือ Swift ก็อีกเรื่อง แต่ถ้ามัน niche ขนาดนั้น จะใช้ Haskell ไปเลยก็ไม่ต่างกัน
  • การบอกว่า if err != nil ไม่ใช่บั๊กแต่เป็นฟีเจอร์ และมันทำให้คุณเห็นทุกจุดที่อาจเกิดปัญหาได้นั้นไม่จริง
    ความเป็นจริงคือมันไม่ได้บังคับ คุณจะเพิกเฉยต่อข้อผิดพลาดก็ยิ่งง่ายเข้าไปอีกถ้าไม่เช็กเอง
    ในแง่วิธีจัดการหรือส่งต่อข้อผิดพลาด Rust ก็ยังเป็นตัวอย่างที่โดดเด่น

    • ใช่เลย ถ้าไม่มีอะไรอย่าง errcheck ข้อผิดพลาดจะถูกมองข้ามได้ง่ายเกินไป ซึ่งมันก็โง่มาก อย่างน้อยควรบังคับให้ต้องทิ้ง error อย่างชัดเจน
      โชคดีที่ทุกโปรเจ็กต์ Go ที่ผมทำในช่วงไม่กี่ปีมานี้ล้วนใช้ golangci-lint ครอบทับการตรวจเช็กแบบสแตติกที่ติดมากับ Go ซึ่งค่อนข้างอ่อนแอ พูดตรง ๆ ว่ามันควรเป็นสิ่งจำเป็นสำหรับทุกโปรเจ็กต์ Go
    • ในจุดนี้ Swift ดีกว่า โมเดลเชิงฟังก์ชันก็คล้ายกัน แต่ฝั่ง Swift ทำให้การส่งต่อข้อผิดพลาดข้ามไลบรารีต่าง ๆ ง่ายกว่า เพียงแต่ก็เป็นเรื่องของข้อดีข้อเสียและทางเลือกการออกแบบ ไม่ใช่ว่าดีกว่าหรือแย่กว่าเด็ดขาด
  • ผมเกลียดกระแสการเขียนแบบนี้จริง ๆ แต่ก็เห็นด้วยกับสารที่บทความพยายามจะสื่อ
    ประโยคที่ว่า “ไม่มี node_modules ขนาดเท่า Volkswagen” ก็จริงแหละ แต่สิ่งนั้นเป็นแค่แคชแพ็กเกจแบบ globalใน ~/go แทนที่จะเป็น node_modules แบบ local ของโปรเจ็กต์

    • แถมยังทำให้โฮมไดเรกทอรีรกอีกด้วย ไม่มีแม้แต่จุดนำหน้า ผมไม่เข้าใจเลยว่าคนยอมรับเรื่องนี้กันได้ยังไง
    • ก่อนจะไปด่าขนาด dependency tree ของ ecosystem ภาษาอื่น ผมอยากบอกให้ลองรัน wc -l go.sum กันก่อนทุกที
  • เปิดหน้าเว็บมาแล้วเห็น “Hey, dipshit.” เป็นอย่างแรก ก็ปิดทันทีเลย

  • มันมีปัญหาเดียวกับบทความยกย่องภาษาโปรแกรมส่วนใหญ่ คือไม่ได้โฟกัสว่าภาษาปัจจุบันยอดเยี่ยมแค่ไหน แต่ไปโฟกัสว่าภาษาที่เคยใช้ก่อนหน้านั้นห่วยแค่ไหนมากกว่า
    ผู้เขียนดูเหมือนจะทุกข์หนักกับ Ruby และ TypeScript หรืออาจรวมถึง Python ด้วย แล้ว Go ก็เข้ามาแก้ปัญหาให้ แต่ผมไม่ได้ใช้ Ruby หรือ TypeScript เลยรู้สึกว่าบทความนี้ไม่ค่อยโดน
    ผมรู้สึกเหมือนอ่านเวอร์ชันดัดแปลงของอะไรแบบนี้มาหลายสิบครั้งตลอดหลายปีที่ผ่านมา ใช้ Haskell สิ เพราะมันมี static types ต่างจาก Python กับ JavaScript ใช้ Rust สิ เพราะ deploy เป็น single binary ได้ ต่างจาก Perl กับ Erlang ใช้ Elixir สิ เพราะมี concurrency กับ channels ที่เหมาะสม ต่างจาก Ruby กับ Tcl
    ผมดีใจที่ผู้เขียนเจอภาษาที่เหมาะกับตัวเอง แต่ผมคงไม่ทำตามคำแนะนำนั้น

    • ดูเหมือนที่นี่จะมีคนจำนวนไม่น้อยที่คิดว่าต้องขาย Go ให้ผู้อ่าน Lobsters ซึ่งสำหรับบางคนอาจให้ผลตรงกันข้ามได้ด้วยซ้ำ
  • ผมรู้สึกมาตลอดว่าค่า zero value ของ Go เป็นข้อเสีย ผมคิดว่าการบังคับให้ผู้ใช้ระบุค่าเริ่มต้นเองจะดีกว่า นอกเหนือจากนั้น ถ้าคิดว่ามันไม่ใช่ OCaml ก็ถือว่าเป็นภาษาที่ดีทีเดียว

    • ผมชอบ zero value และคิดว่ามันค่อนข้างฉลาดนะ แต่สิ่งที่ขาดจริง ๆ คือฟีเจอร์ตั้งค่าเริ่มต้น ตัวอย่างเช่น มันยากมากที่จะ marshal อ็อบเจ็กต์ JSON ที่ bool ซึ่งหายไปควรมีค่าเป็น true
  • ประสบการณ์ตอน deploy และ compile นั้นยอดเยี่ยม แต่ผมเกลียดการเขียนตัวภาษาเองมาก ทุกครั้งที่ใช้เป็นประสบการณ์ที่แย่จริง ๆ มีภาษาอื่นที่ข้อจำกัดไม่หนักเท่า Go แต่ให้ประสบการณ์การ deployที่ดีแบบนี้ไหม?
    หรือผมกำลังพลาดอะไรบางอย่างใน Go?
    ช่วงหลังผมลอง deploy แอป Rails เล็ก ๆ แล้วต้องตั้งค่าเยอะมาก เลยยิ่งชัดว่า Go มีข้อดีตรงนี้จริง ๆ

    • ช่วงนี้ผมเริ่มคอมไพล์โปรเจ็กต์ Rust ไปที่ x86_64-unknown-linux-musl แบบนี้จะได้ไบนารีแบบ staticที่เอาไปรันบนเครื่อง Linux 64 บิตเครื่องไหนก็ได้ จากนั้นก็ย้ายด้วย scp แล้วรันเลย
      ตอนนี้ยังมีปัญหาเรื่องต้องจองพอร์ตและสตาร์ตเองด้วยมืออยู่ แต่กะจะแก้ด้วยมนตร์นิดหน่อยของ systemd
    • ในแง่ประสบการณ์ deploy ที่บริษัทเราใช้ nix bundler แล้วได้ผลดีเกินคาดมาก ขอบอกบริบทก่อนว่าพวกเรากำลังทำแอป GUI ด้วย Qt6
      พอใช้ bundler ก็ทำไฟล์รันเดี่ยวได้ เอาไปวางบนเครื่อง Linux ที่เป็นคนละดิสโทรกันได้เลย ต่อให้ไม่ได้ติดตั้ง Qt ไว้ ผู้ใช้ก็แค่รันไฟล์นั้นแล้ว GUI ทั้งหมดก็ทำงาน
      แต่ก็มีข้อแม้เรื่องไดรเวอร์ OpenGL อยู่บ้าง ยังทำได้อยู่ เพียงแต่ซับซ้อนกว่าระดับ “ก็อปปี้แล้วรัน”
  • ปัญหาใหญ่ที่สุดคือ Go ชอบอ้างว่าถูกออกแบบมาเพื่อ concurrency แต่กลับมีraw pointers ที่เผลอแชร์กันได้ง่ายฝังอยู่ในภาษา

  • ความน่าเบื่อในตัวมันเองไม่ใช่ปัญหา แต่ผมว่า Go กลับล้มเหลวเป็นพิเศษในการเป็นภาษาที่น่าเบื่อจริง ๆ
    บอกว่า “ไม่มี decorators” ก็จริง แต่มี struct tags กับ reflection อยู่ ซึ่งยากจะรู้ว่ามันทำงานร่วมกันยังไงจนกว่าจะลองรัน
    structural interfaces กับ reflection เป็นแหล่งกำเนิดความน่ากลัวของพฤติกรรมที่เปลี่ยนจากที่ไกล ๆ คุณแค่เพิ่มเมธอดผิดตัวเดียวใน struct พฤติกรรมของไลบรารีก็อาจเปลี่ยนไปหมด
    ในมุมของการทำเอกสารก็ดูแปลก ทำไมถึงไม่อยากให้ชัดเจนไปเลยว่าชนิดนี้ตั้งใจให้ตรงกับ interface ไหน?
    แล้วทำไม goroutines ถึงไม่เรียกมันว่า threads ไปเลยก็ไม่รู้
    แล้ว channels ทำไมต้องเป็นฟีเจอร์ของภาษาด้วย? ผมเดาว่าเพราะกว่าจะยอมรับว่า generics มีประโยชน์มากกว่าแค่ type สักสามแบบ ก็ช้าไปตั้ง 10 ปี

    • goroutines ไม่ใช่ threads แต่มันเป็น abstraction ที่เบากว่าและทำงานอยู่บน thread pool ดังนั้นจึงสร้าง goroutines เป็นพัน ๆ ตัวได้ง่าย
      ที่ channels เป็นส่วนหนึ่งของ runtime ก็น่าจะเพราะตัว scheduler ของ goroutines ต้องรู้จัก channels เพื่อจะปลุก goroutine ฝั่งรับได้ง่ายขึ้นเมื่อ channel ไม่ว่างแล้ว คิดว่าทำแบบนี้น่าจะง่ายกว่า
    • goroutines ก็เพราะมันคือgreen threadsที่มีเครื่องมือเสริมติดมาด้วย