ห้าม Compare Struct ใน Go

หลายเดือนก่อนทีมทำเรื่อง data encryption ใน Go ซึ่งเราจะมี struct EncryptedString ซึ่งเก็บ string แบบเข้ารหัสไว้แล้ว

ปัญหาก็คือเรากังวลว่า developer อาจจะคิดว่ามันใช้งานเหมือน string ปกติแล้วไป compare (encryptedA == encryptedB) ซึ่งมันอาจจะเท่าหรือไม่เท่ากันก็ได้ขึ้นอยู่กับ internal state ของ string ก็ต้องแจ้ง developer ไปว่าให้ใช้ Equal() เสมอ อย่าใช้ ==

บังเอิญช่วงนั้นผมเขียน Protobuf อยู่พอดีเลยไปเห็น google.golang.org/protobuf/internal/pragma ซึ่งผมว่ามันมีแต่ hack เทพๆ ที่อ่านแล้วฉลาดขึ้นเลย

DoNotCompare

Hyrum’s Law บอกว่าถ้ามีคนใช้ API มากพอ ต่อให้ไม่ได้บอกว่าใช้แบบนี้ได้ มันจะมีคนไปใช้ตามพฤติกรรมที่สังเกตได้เสมอ ซึ่งเราอาจจะเคยได้ยินว่าบางระบบอาจจะถึงกับต้อง emulate bug เพื่อให้มันใช้งานได้ เช่นใน StarCraft Remastered นั้น Blizzard ถึงกับต้องจำลอง buffer overflow เพราะ map เก่าๆ บางอันใช้

ในเคสของ Protobuf แปลว่าถ้าใช้ == กับ Protobuf message ได้แถม return ค่าถูกต้อง ก็คงจะมีคนใช้ == แน่นอน พอออก version ใหม่ที่เปลี่ยนโครงสร้างของ struct ก็พัง ทั้งๆ ที่ไม่เคยบอกว่าใช้ == ได้ ดังนั้น Go Protobuf เลยมี “pragma” หรือสูตรสำเร็จที่ฝังเข้าไปใน struct เพื่อทำให้พฤติกรรมต้องห้ามนั้นมี error หรือ warning เกิดขึ้นได้

ในบล็อคนี้ผมจะพาไล่ทั้ง 4 pragma ในแพคเกจขณะที่เขียนนี้ พร้อมทั้งเฉลยว่ามันทำได้อย่างไร อ่านแล้วผมแนะนำให้ลองคิดตามก่อนที่จะอ่านเฉลยครับ

โค้ด DoNotCompare มีอยู่ว่า

type DoNotCompare [0]func()

โดยเวลาใช้งานให้ฝังเข้าไปใน struct เช่น

type X struct {
    Value int

    DoNotCompare
}

เมื่อเรา compile จะได้ข้อความว่า

./prog.go:17:20: invalid operation: a == b (struct containing DoNotCompare cannot be compared)

เหตุผลที่เป็นอย่างนี้เพราะใน Go เราไม่สามารถเปรียบเทียบ function ได้

./prog.go:17:31: invalid operation: fmt.Println == fmt.Println (func can only be compared to nil)

การ compare struct นั้นมันจะ compare ทุก field ภายใน struct เมื่อเจอ field นี้ที่ type เป็น function ซึ่ง compare ไม่ได้แล้ว compiler จึงจะไม่ยอมให้ compile

ส่วน [0] ด้านหน้า ไว้เดี๋ยวจะเฉลยครับ…

DoNotCopy

ใน Go เราอาจจะมีการ pass by value ในบางครั้งซึ่งมันจะ copy object เข้าไปใน function เช่น

type X struct {
    Value string
}

func RecvX(v X) {
    v.Value = "changed"
}

func main() {
    x := X{value: "init"}
    RecvX(x)
    fmt.Println(x.Value)
}

ในกรณีนี้ เนื่องจาก RecvX ได้รับ X แบบ pass by value ทำให้ v เป็น copy ของ x ใน main เมื่อ print ออกมาจะได้ผลลัพท์ว่า “init” ไม่ใช่ “changed”

เพื่อป้องกันการใช้งานแบบ pass by value โค้ด DoNotCopy จึงมีดังนี้

type DoNotCopy [0]sync.Mutex

โค้ดนี้จะต่างกับ DoNotCompare คือสามารถ compile ผ่านได้ตามปกติ แต่ถ้ารัน go vet แล้วจะมี error ดังนี้

./prog.go:16:14: RecvX passes lock by value: play.X contains sync.Mutex

สำหรับคนที่เคยใช้งาน sync.Mutex แล้วน่าจะทราบดีว่าห้าม copy sync.Mutex เด็ดขาดเนื่องจากจะทำให้มี lock 2 ชุดแทนที่จะมีชุดเดียว ซึ่ง go vet จับให้อยู่แล้ว ดังนั้นการฝัง sync.Mutex เข้าไปใน struct ถึงแม้ไม่ได้ใช้ก็ทำให้ go vet ทำงาน

คำถามจากหัวเรื่องที่แล้วคือ แล้ว [0] คืออะไร? syntax นี้ผู้เริ่มต้น Go บางคนอาจจะเรียน บางคนอาจจะข้ามไปเลยเพราะไม่ค่อยได้ใช้ syntax นี้คือ fixed size array ใน Go เช่น [10]int คือ slice ที่มีสมาชิกเป็น int 10 ตัวไม่สามารถเพิ่มลดได้ (และมันเป็นคนละ type ไม่ใช่ slice ใช้แทนที่ slice ปกติไม่ได้)

เนื่องจากจำนวนสมาชิกเป็นค่าที่ทราบตั้งแต่ compile ทำให้ compiler สามารถจอง memory ให้สมาชิกทั้ง 10 ตัวได้ล่วงหน้า ในกรณีของ pragma 2 ตัวก่อนหน้านี้เราระบุเป็น [0] ก็คือไม่มีสมาชิก ทำให้ compiler ไม่จองพื้นที่สำหรับเก็บข้อมูลไว้เลย ทำให้ไม่มี overhead จากการใช้งาน

NoUnkeyedLiterals

ปัญหาถัดมาที่เจอคือมันจะมีคนมักง่าย แบบนี้…

type X struct {
    Value1 string
    Value2 int
}

func main() {
    X{"v", 1}
}

พอสร้าง struct โดยไม่ระบุชื่อ field ปัญหาที่ตามมาคือถ้าจะเพิ่ม field คนที่เพิ่มจะกรีดร้องเพราะต้องไล่ตามแก้ของเก่าให้หมด

โค้ด NoUnkeyedLiteral ง่ายๆ แค่

type NoUnkeyedLiterals struct{}

ใช่แล้ว มันคือ struct ง่ายๆ ไม่มีอะไรเลย เนื่องจากมันจะทำให้ struct มี 3 field (ไม่ใช้ 3 field) โค้ดด้านบนจึงจะเจอ

./prog.go:14:27: too few values in X{...}

ทำให้ไม่สามารถระบุแค่ 2 field ที่ใช้งานได้ ต้องระบุค่าของ NoUnkeyedLiteral ด้วย ก็คือต้องพิมพ์

X{"v", 1, NoUnkeyedLiteral{}}

ซึ่งก็ควรจะเอะใจได้แล้วนะว่าเค้าไม่ให้ทำแบบนี้ !!

DoNotImplement

Pragma ตัวสุดท้ายคือ DoNotImplement ซึ่งไม่อนุญาตให้แพคเกจอื่น implement interface นี้ เนื่องจากใน Go นั้น interface ไม่จำเป็นต้องประกาศ implement ขอแค่มี method ตรงกันเป็นอันใช้ได้ จึงไม่สามารถประกาศ interface เป็น private ได้

โค้ดของ DoNotImplement หน้าตาแบบนี้

type DoNotImplement interface{ ProtoInternal(DoNotImplement) }

โดยการใช้งานก็ให้ฝังเข้าไปใน Interface อื่น

type Printable interface {
    Print()

    DoNotImplement
}

ผมใช้เวลาหลายชั่วโมงพยายามทำความเข้าใจโค้ดนี้ ใคร declare ProtoInternal? แล้วมัน recursive มันช่วยยังไง?

ถ้าจัดรูปใหม่แล้ว มันจะเป็น

type DoNotImplement interface {
    ProtoInternal(value DoNotImplement)
}

ก็คือเป็น interface ที่มี 1 method ชื่อ ProtoInternal รับ argument 1 อย่างเป็น type ตัวเอง

แต่ เอ มันก็แก้ด้วยการเขียน implementation ง่ายๆ แบบนี้

func (x *X) ProtoInternal(value DoNotImplement) {}

หรือเปล่า….

ผมอ่าน docs อยู่ 2 ชั่วโมงก่อนจะไปสังเกตชื่อแพคเกจ google.golang.org/protobuf/internal/pragma

ใน Go แพคเกจจะไม่อนุญาตให้ import package ที่มี path internal มาจากแพคเกจอื่นๆ ดังนั้นถ้าจะ implement interface นี้ได้จริง ก็ต้อง import DoNotImplement มาใส่ใน argument ให้ได้ด้วย และเนื่องจากว่ามันเป็น internal จึงมีเฉพาะ package ที่ได้รับอนุญาตเท่านั้นจึงจะ import ได้

ทริคนี้ยังใช้ใน NoUnkeyedLiteral ในหัวข้อก่อนหน้าด้วย โดยถ้าจะดันทุรังใช้ X{"v", 1, NoUnkeyedLiteral{}} ก็จะเจอปัญหาเหมือนกันว่า import NoUnkeyedLiteral ไม่ได้

Genius

ทั้ง 4 ทริคนี้ผมว่าเป็นโค้ดที่เขียนได้ฉลาดมากๆ เหมือนเป็น puzzle และตอนอ่านผมก็คิดว่ามันลับสมองมากๆ ที่จะไล่ดูกลวิธีของผู้สร้างว่าเค้าวางกับดักโดยใช้แค่ Go standard tools ได้อย่างไร

ในทีมผมก็คุยกันแล้วว่าทริคลักษณะนี้อาจจะยังไม่อยากใช้เท่าไรนักเพราะภายในบริษัทอาจจะยังพอตกลงกันหรือตรวจกันใน code review ได้โดยไม่ต้องพึ่ง hack แต่ในระดับ protobuf ที่เป็น library ใช้กันแพร่หลายแล้วการบังคับทางเทคนิคนี้ก็เป็นเรื่องจำเป็นที่จะไม่ให้คนเขียนโค้ดแบบ genius เล่นท่าพิสดารที่อัพเกรดไม่ได้

Programming Languages I Write

ตอนนี้เขียนโปรแกรมอยู่หลายภาษา ก็คิดว่าอยากลองเขียนดูบ้างว่าเราคิดว่าจุดแข็งของแต่ละภาษาคืออะไร แล้วเราเลือกใช้มันยังไง

กฎส่วนตัวเราเวลาบอกว่าเขียนภาษานี้ “เป็น” พอจะใส่ใน Resume คือถ้า Ecosystem ในเรื่องนั้นมันพร้อมเราสามารถทำ Task อะไรก็ได้ในภาษานั้นๆ และเขียนออกมา Idiomatic เฉพาะตัวแบบที่ภาษานั้นๆ เขียนอยู่ด้วย

หลักๆ ที่เขียนเลยก็จะมี

  • Python
  • Go
  • JavaScript & TypeScript

ภาษาอื่นๆ ที่เคยเขียนก็จะมี Java, C, C++, Rust, Kotlin, PHP แต่ในบทความนี้คงจะไม่พูดให้ครบทุกอัน

Python

Rule of thumb ตอนนี้คือถ้ามันจะต้องต่อ database เราจะเลือกใช้ Python และ Django เท่านั้น ไม่มีอะไรที่ทำให้เราทำงานเสร็จเร็วเท่า Django อีกแล้ว

นอกจาก Django แล้ว Python ฝั่ง Data Science ก็ค่อนข้างแข็ง แต่เราไม่ค่อยได้ใช้เท่าไร แล้วก็ยังทำ scripting ต่างๆ ได้เร็ว

ข้อเสียของ Python คือมันเป็นภาษาที่ปวกเปียกมาก

Package manager มันอยู่ระหว่าง requirements.txt ที่ใช้ได้แต่ค่อนข้าง manual และไม่มี lock กับ Poetry ที่ยังไม่ดัง (และคนไปเลือกใช้ Pipenv ที่เลิกทำแล้วมากกว่า) และ Virtualenv มันค่อนข้าง Hack หน่อยๆ เมื่อเทียบกับภาษาใหม่ๆ ที่เลิกมี global package repository แล้ว

Import syntax ที่ถ้ามีไฟล์อยู่ folder ข้างๆ จะนั่งงงมากว่าต้อง import ยังไง (มันไม่มี “absolute” import มีแต่ relative โดยใช้ syntax แปลกๆ)

Type checking ที่ยังตั้งไข่มากๆ ถึงจะทำมาหลายปีแล้ว และ syntax ที่มันแปลกๆ หน่อย

Speed ของมันที่ค่อนข้างช้าในการ execute ซึ่งปกติก็จะไม่ค่อยใส่ใจ แต่ก็มีครั้งนึงที่ทำให้พลาดข้อใน Codejam มาแล้วเพราะโค้ดเหมือนกันรันใน Python เกิน 5 นาที ไม่ผ่าน ไปเขียนใหม่ใช้วิธีเดิมใน C รันจบใน 30 วินาที

Web server deployment มันที่ยังต้อง deploy WSGI container เหมือนภาษาโบราณๆ ไม่ใช่ one click และก็จะติดเรื่อง thread pool ที่ต้องมาจูน

JavaScript

ผมไม่ชอบ JavaScript เท่าไรนัก แต่จำเป็นต้องเขียนเพราะมันใช้ได้หลายที่มากๆ มันเป็นภาษาเดียวที่รันได้ใน Browser เวลาจะทำอะไรที่เขียนครั้งเดียวใช้ได้ทุกที่ต้องใช้ JavaScript เลย

ข้อดีของ JavaScript คือ syntax มันค่อนข้างมาตรฐานไม่หวือหวา แล้วพอบวก TypeScript มามันทำให้การ code project ใหญ่ๆ ทำงานงายขึ้นเพราะ editor จะฉลาดขึ้นเยอะ

ข้อเสียของ JavaScript คือภาษามันค่อนข้างฟรีสไตล์และมันไม่เก่ง Class กลายเป็นว่าคนชอบเขียนเป็น functional แต่มันก็ไม่ใช่ภาษาที่ออกแบบเป็น functional มาตั้งแต่แรก โค้ดมันก็เลยเละมาก

แล้วที่ผมไม่ชอบ functional ใน JavaScript มากคือการที่เราเอา higher order function ที่ add functionality ทีละนิด แต่ใช้ซ้อนกันเยอะๆ เวลาไล่โค้ดมันจะงง flow มากว่ามันเข้าแล้วอะไรโผล่เข้ามาตอนไหน รวมถึง syntax มันที่จะเป็น a(b(c(d))) ซึ่งไม่ค่อยเป็นธรรมชาติในการอ่านเพราะ functionality หลักมักจะอยู่ที่ d

นึกภาพว่าถ้า Unix pipe มันเขียนว่า wc(tar('xf', gzcat(file)))ก็คงใช้ลำบากกว่า gzcat file | tar xf - | wc

แล้วการเขียน helper มาทำให้มันกลายเป็นซ้ายไปขวา ก็ทำได้ยุ่งยากใน Javascript เพราะถ้าเราเขียน chain(a, instance.methodB, c, d) this ใน methodB ก็จะผิดอีก ต้องมา bind ก่อน ซึ่งไม่มีภาษาอื่นทำกัน (แล้วผมก็ยังไม่เคยเห็นว่ามี use case ไหนเลยที่จะใช้มันโดยไม่ bind)

สรุปคือมันทำได้ทุกอย่างแต่ไม่เก่งสักอย่าง แต่ก็ยังต้องใช้มันอยู่เพราะใน Browser มันผูกขาด

Node.js

ผมจำไม่ได้ว่าสมัยที่ Node.js เพิ่งมาผมคิดกับมันไว้ยังไง (สมัยนั้นเขียน PHP กับ Web.py) น่าจะไม่ชอบที่เอา JavaScript ไปรันบน server เพราะมันช้า

แต่ในปัจจุบันปรากฏว่ามันเร็วมาก เร็วกว่า Python เสียอีกเพราะ JavaScript ใน browser มีเงินลงทุนมหาศาลจากบริษัทเว็บต่างๆ

ข้อดีของ Node คือ async io ซึ่งมาแรกๆ หลายคนไม่เชื่อว่าไม่ต้องมี application server คั่นหน้าแล้ว เอา Node รับ load ได้ตรงๆ เลย

แต่ข้อเสียของ async io ใน Node คือ syntax มันไม่ค่อยสวยเท่าไรนัก ไม่ว่าจะเป็น Callback หรือ Promise + Await ก็ตามมันก็ไม่ดูดีเท่า Sequential (a().b().c() vs. await (await (await a()).b()).c())

นอกจากนี้เนื่องจาก standard library ที่มีให้ค่อนข้างจำกัด ทำให้เกิด npm ecosystem ที่บูมมากๆ อะไรก็อยู่บน npm ได้หมดแม้แต่ application อย่าง Chromium ก็โหลดผ่าน npm ได้ ข้อเสียก็คือแต่ละคนก็เลือกใช้ lib คนละตัวแตกต่างกันไป รวมถึงพอแยกกัน maintenance คุณภาพก็แตกต่างกันออกไปตั้งแต่ระดับดีมากๆ อย่าง React ไปจนถึง left-pad ที่คนทำอยู่ดีๆ ก็เอาลง

พอ community มันเปิดกว้าง มันก็กลายเป็นว่า project structure เปิดมาแต่ละอันไม่ซ้ำกันเลย เวลาอ่านก็ต้องไปนั่งไล่ก่อนว่าทางเข้าทางออกอยู่ตรงไหนบ้าง แล้วแต่ละคนก็จะทำท่าไม่เหมือนกันเลยเพราะคนไม่ค่อยใช้ Framework (อาจจะมีใช้ Next/Nuxt.js บ้างแต่ก็ไม่ใช่ทุกคนจะชอบ)

ข้อจำกัดอีกเรื่องของ Node คือมันทำอะไรที่ CPU bound ไม่ได้เลยเพราะมันเป็น single thread แถมยังไม่มีวิธีแตก thread เลยแม้แต่วิธีเดียวเนื่องจากมันมี Global lock เหมือน Python (แต่ Python ยังมี multiprocess ให้ใน standard library) แต่คิดว่าเร็วๆ นี้น่าจะมา

ตอนนี้จุดเดียวที่เรายังเลือก Node.js อยู่คือ Socket.io เนื่องจากภาษาอื่นๆ ในยุคก่อนมันทำ Streaming ไม่ได้ (thread pool จะเต็มเอา) พอมาถึงยุคนี้ที่ใครๆ ก็ทำ async แล้วก็เลยโดดข้ามไป WebSocket เลย จะมีแต่ Socket.io เท่านั้นที่ทำได้ทั้ง Long polling และ WebSocket ได้ในเวลาเดียวกัน และใช้โค้ดเดียวกัน

Go

ช่วงที่เขียน Rust ผมบอกว่าจะไม่เขียน Go เดี๋ยวใจแตก แล้วก็เป็นอย่างนั้นจริงๆ

Go แก้ทุกปัญหาของ Node ที่ผมบ่นไว้ด้านบนได้เกือบหมด

ภาษามันเป็น Static typed ในขณะเดียวกัน map เป็น first class data structure ของภาษา ไม่เหมือนภาษายุคก่อนหน้า และมันยังไม่กลัวที่จะใช้ interface{} type

Go มี Standard library + golang.org/x ที่ค่อนข้างดีทำให้ third party ไม่ต้องนั่งตบตีกันว่า HTTP client ต้องใช้ตัวไหน (Go ก็มีให้เลือก แต่ส่วนมากคนก็ยังเชียร์ http.Client ใน standard library อยู่)

Syntax ของภาษาที่เรียบง่ายมากๆ ทีม Go เคยบอกว่าเขียน Go คือเขียนโค้ด ไม่ใช่ไปนั่งเขียน Type

Go มี gofmt และ compiler เองก็เป็น linter ในตัว (no unused variable, enforce ชื่อ function ตัวใหญ่เป็น public เป็นต้น) ทำให้โค้ด Go ไม่ว่า project ไหนส่วนมากก็แทบจะเป็นแบบเดียว

ตั้งแต่เขียนโค้ดมาคิดว่าเรา contribute Go pull request มากที่สุดแล้วถึงจะมาเขียนหลังสุด เพราะมันไม่ต้องกลัวว่า maintainer จะด่าอะไรเราในโค้ดมั้ย

(แอบชอบสไตล์ modulename.FunctionName() ของ Go ด้วย อ่านแล้วมันเคลียร์เลยทันทีว่า function มาจากไหน และตัวใหญ่ทำให้สะดุดตาโดดข้ามไปอ่านง่าย ต่างกับใน JS ที่คนชอบ destructure เอาแต่ functionName มา มันเพิ่ม cognitive load ต้องจำว่าตัวแปรนี้มาจากไหน)

วิธีแก้ปัญหา async แล้ว syntax ไม่สวยของ Go คือเลิกทำ async ทุกอย่างกลายเป็น sync อะไรที่อยากจะรันข้างหลังค่อยสั่งเปิด goroutine ซึ่งเพิ่มแค่คำว่า go ข้างหน้า (เทียบกับ Python thread นี่เขียนกันหลายบรรทัดเลย)

ที่คนเขียน Go อาจจะไม่รู้คือที่เห็น Go เป็น sync จริงๆ ด้านล่างมันก็เป็น async คล้ายๆ กับที่ Node ทำ แล้วมันใช้ scheduler เป็น abstraction ทำให้มันกลายเป็น sync (เหยด!)

แล้ว Go ไม่มี Global lock เหมือน Python หรือ JavaScript มันทำให้ performance ยิ่งดีขึ้นไปอีก แต่ก็อาจจะต้องระมัดระวัง race condition เองมากขึ้น สำหรับเราเราคิดว่ามันตื่นเต้นและเป็นประสบการณ์ที่อยากเรียนรู้ เพราะภาษาอื่นที่เขียนมี global lock หมดเลย

เวลา Deploy Go ก็ทำได้ง่ายกว่าภาษาอื่นๆ มาก เพราะมัน compile มาได้ binary ตัวเดียวใหญ่ๆ (20-200MB) แล้วคลิกเดียวรันได้เลย

ทุกวันนี้เราเลยพบว่ามาเขียน Go บ่อยขึ้นมาก แทบทุกอย่างที่ไม่ต้องแตะ Database ก็จะไปเขียน Go แม้แต่ scripting บางทียังเลือกใช้ Go เพราะมัน Deploy ได้ง่ายกว่าเยอะ

ข้อเสียของ Go คือ module ซึ่งมันมาช้าไปหน่อย และก็ยังออกแบบมาไม่ค่อยดีอยู่ดี ปัญหาที่เห็นคือระบบ versioning ที่ไม่เคลียร์เท่า Node.js และการใช้ URL import ทำให้เวลา fork ต้องแก้ทั้ง project (แล้วเกิดอยากจะ merge กลับจะทำได้ยาก)

Go ไม่มี generic ทำให้บางอย่างทำได้ยาก โดยเฉพาะการ search in array ที่เป็น operator ใน Python แต่ใน Go ต้องนั่งเขียน loop ทุกครั้ง

Go ไม่มี implements ที่เอาไว้เช็คว่า function/struct นี้ satisfy interface แล้วหรือยัง ถ้าไม่ตรง มันจะไป error ตรง call site แทนที่จะ error ที่ struct (ซึ่งลำดับการเขียนโค้ด เรามักจะเขียน struct ให้เสร็จก่อนแล้วค่อยไป create + pass ไปยังคนที่ใช้)

แล้วด้วยความที่มันเป็น static typed ก็คิดว่ายังไม่อยากเอามันไปทำงานที่ใช้ JSON มากๆ เท่าไร

Java

ยังไม่แน่ใจว่าไม่ชอบ Java หรือไม่ชอบ Spring แต่ที่แน่ๆ คือไม่ชอบที่ Java เพราะพยายาม violate YAGNI ทุกวันนี้ยังตั้งคำถามอยู่ว่าถ้าให้เรา rebuild ecosystem Java ให้ใช้ standard เหมือน Python (แต่ไม่แก้ compiler) จะอยากเขียน Java มั้ย

ปัญหาของ Java คือ type system มัน strict เกินไป เวลาจะเพิ่มนิดๆ หน่อยๆ ทำไม่ได้ เช่น ถ้ามี request object แล้วอยากจะฝากข้อมูลไปบน request object ก็ต้องทำ object อีกตัวมาครอบแล้ว delegate เข้าไปใน request (เทียบกับ JavaScript/Python ที่ใช้ req.data ได้เลย หรือ Go ก็มี embed struct ไปบน struct อีกตัว)

คนเขียน Java ก็เลยจะนิยมว่าทำ interface ไว้ให้ทุกอย่าง “เผื่อแก้” data ต่างๆ บน class อื่น ต้องวิ่งผ่าน getter เท่านั้น (ใน Go เรา mutate field บน http.Request กันสนุกเลย)

สองคือภาษามันเก่าแล้วมันไม่มี first class map เหมือนภาษาใหม่ๆ ทำให้ท่าที่จะเขียนโค้ดง่ายขึ้นกลับกลายเป็นโค้ดยุบยับที่อ่านลำบาก

สามคือภาษามันไม่มี first class function & class เวลาส่ง function และ class เข้าไปมันจะฟีลเหมือนเขียน reflection หน่อยๆ ซึ่งทำให้หลายๆ ท่าลำบากในการเขียน

หลายๆ ฟีเจอร์พวกนี้ถูกแก้ด้วย code generation บนภาษาใหม่ๆ เช่น Kotlin, Groovy หรือ Scala ที่ข้างหลังก็ไป generate class ออกมาอยู่ดี

PHP

ตอนเรียนจบ PHP เคยอยู่ใน resume ทุกวันนี้หลายๆ ครั้งก็ยังวิเคราะห์ PHP design อยู่ว่ามันพลาดไปตรงไหน

เราคิดว่า Design ของ PHP ออกแบบมาน่าสนใจ มันเป็นภาษากระแสหลักภาษาเดียวที่ออกแบบมาให้เขียนเว็บเพียงอย่างเดียว

ไม่มีภาษาอื่นไหนที่เขียน hello world เป็นหน้าเว็บได้ใน 1 บรรทัด อย่างน้อยๆ ก็ต้องเขียนส่วนที่รับ request object เข้ามาก่อน แต่ใน PHP มันอยู่ใน global มาตั้งแต่แรก

แล้วถ้าอยากจะ set header สักตัว ก็แค่เพิ่ม header("Content-Type: application/json"); จากหนึ่งบรรทัดกลายเป็นสองบรรทัด

ตัวภาษาเองยัง compact พอที่จะเป็น template engine ในตัวด้วย อย่างที่ WordPress ทำอยู่

แต่ community PHP อยู่ดีๆ ก็อยากเป็น Java แล้วทิ้งพวกนี้ไปโดยไปทำ Symfony และ Laravel มา

จากภาษาที่ echo ก็ออกหน้าจอได้แล้ว กลายเป็นว่าต้อง return Response object กลับไปเหมือนภาษาอื่นๆ แถมยังพยายามทำเป็น OOP จัดๆ ทั้งๆ ที่ตอน PHP4 ไม่มี class เลย พวก function ต่างๆ เช่น echo หรือ header กลายเป็นสิ่งที่ไม่ควรใช้

ก็ไม่รู้ว่า PHP พลาดไปตรงไหน เพราะ Zend หรือเปล่านะที่พยายามทำให้ PHP กลายเป็นการค้าแล้วก็ทำให้มันขายได้ หรือเพราะช่วงเปลี่ยนผ่านที่คนยังไม่ซื้อ VPS กันแล้วจำเป็นต้องใช้ PHP (แล้วที่ JavaScript ทุกวันนี้มันเป็นแบบนี้ ก็เพราะเหตุผลนี้หรือเปล่านะ?)

ทุกวันนี้ก็ยังฝันอยู่ว่าถ้าสร้าง PHP ขึ้นมาใหม่ในยุคนี้จะออกแบบมันยังไงดี แล้วมีใครทำแล้วหรือยังนะ?

ที่เอา PHP ออกจาก resume ตอนนี้เพราะตั้งแต่มันมี Symfony และ Laravel มา ก็ไม่ได้ตามแล้ว เคยเขียนอยู่ 2 project ใน stack นั้นได้ ก็ยังคิดว่า Django ดีกว่า

สองคือ คนเขียน PHP แย่ๆ มีเยอะในประเทศนี้เลยไม่อยากเสี่ยงไปทำงานกับคนพวกนี้ (เหมือนที่ 37signals ใช้ Ruby เพราะใครเขียนเป็นในยุคนั้นคือเซียน จะได้คัดกรองคนง่าย) สมัยที่เคยทำ freelance มา ทุก project ที่มี PHP code เรา penetration test แล้วเจอช่องโหว่ทุกอัน พอแจ้งไปก็มักจะมีข้ออ้างที่ไม่สนใจจะแก้ (เคยเจอขนาดว่าแก้ให้แล้วมา overwrite ทับ)

สามคือคนที่ยังใช้ PHP อยู่ ส่วนนึงคือติดอยู่ใน environment ที่ต้องใช้ PHP (และมักจะเป็นรุ่นเก่า) เราคิดว่าพอหลุดพ้นจาก environment นั้นไปได้แล้ว มันเปิดกว้างให้ใช้ภาษาอะไรก็ได้

Rust

เคยจับ Rust มาผ่านๆ ด้วยเหตุผลว่าเราไม่มีภาษาไหนเขียน low level เลย (ตอนนั้นยังไม่ได้เขียน Go)

ปัจจุบันก็คงคิดว่าไม่กลับไปเขียนแล้ว เพราะ ecosystem มันพร้อมเลยสำหรับงาน high level และ borrow checker ที่ทำให้เรา unproductive (ถ้ายอมมี garbage collection แล้วเขียนโปรแกรมได้เร็วขึ้น 10 เท่า สำหรับงานที่ผมทำมันไม่เลือกก็พลาดแล้ว)

It Depends

สรุปว่าเราเลือกใช้ตามนี้

  • ถ้าทำเว็บ ต่อ Database ใช้ Python และ Django
  • ถ้าทำอะไรที่ไม่ต้องต่อ Database (เช่น stateless API) อาจจะใช้ Go
  • บน browser ยังคงต้องใช้ JavaScript อยู่
  • Scripting ต่างๆ ถ้าเอาเร็วใช้ Python
  • แต่ถ้าจะเอาความ Deploy ง่าย ใช้ Go

ตอนนี้ยังไม่มีแผนจะเรียนภาษาเพิ่ม เพราะดูจะยังไม่มี use case ไหนขาดตกจนใช้ภาษาที่เขียนแก้ไม่ได้เลยเลยสักภาษา แล้วพอเรียนภาษาใหม่แต่ไม่ได้ใช้มันก็จะลืม (เคยจะลองเขียน Ruby แต่พอพบว่ามันทับซ้อนกับ Python ก็ไม่ได้ใช้ ลืมหมดแล้ว)

สุดท้ายแล้วถึงแต่ละภาษาจะมีจุดแข็ง – จุดอ่อนต่างๆ และมี clear use case ที่เราเลือกใช้ แต่หลายๆ ครั้งก็จะเลือกตามนั้นไม่ได้ถ้าทำหลายคน แล้วคนอื่นไม่ได้เขียนด้วย อันนั้นก็ต้องคุยกันอีกที