Sunday, September 8, 2024

แนวทางการใช้ Go package โดย Jaana Dogan(rakyll)

Programming Languageแนวทางการใช้ Go package โดย Jaana Dogan(rakyll)


Jaana Dogan หรือ rakyll คือใคร ปัจจุบันเธอทำงานที่ github ในฐานะ Engineer ที่เป็นที่รู้จักมาก ก่อนหน้าที่เคยทำงานที่ AWS และก่อนนั้นก็ที่ Google และเป็นทีมที่ทำภาษา Go ด้วย สามารถติดตามเธอได้ที่ rakyll.org และ https://github.com/rakyll โดยต่อจากนี้จะเรียกเธอด้วยชื่อ rakyll แทนนะครับ

บทความเกี่ยวกับเรื่องนี้ต้นฉบับอ่านได้ที่ https://rakyll.org/style-packages/

rakyll เริ่มต้นด้วยการเกริ่นก่อนว่าใน Go นั้น การตั้งชื่อ และการวางโครงสร้างมีความสำคัญ ไม่แพ้เรื่องอื่นๆเลยแม้แต่น้อย เพราะการวางโครงสร้างโค้ดที่ดี จะทำให้มันอ่านง่่าย ดูแลง่าย ค้นหาอะไรก็ง่าย ซึ่งการที่เราสามารถจัดระเบียบโค้ดให้ดี มันมีความสำคัญมากพอๆกับการออกแบบ API ให้ดีเลยทีเดียว นั่นก็เพราะว่า ไม่ว่าจะชื่อที่เราตั้ง ตำแหน่งที่มันถูกวาง โครงสร้างของมัน จะเป็นด่านแรกที่ไม่ว่าจะคุณหรือใคร ก็ต้องปะทะกับมันก่อนอยู่แล้ว

และเป้าหมายของเธอในการอธิบายเรื่องนี้ ก็ไม่ได้ต้องการจะสร้างกฏเกณฑ์ใดๆให้ต้องปฏิบัติตาม เป็นเพียงการให้แนวทางปฏิบัติในแบบที่ควรจะเป็น และสุดท้ายทีมจะเป็นผู้ตัดสินใจเลือกเองอยู่ดี เอาล่ะ เรามาเข้าเรื่องกันเลยดีกว่า

Packages

Go ใช้ package ในการจัดระเบียบ โดยหลักการแล้วก็เหมือนกับการที่เราจัดการไฟล์และ folder ในเครื่องคอมพิวเตอร์ของเราเลยครับ เช่นเมื่อเราสร้าง package ขึ้นมา 1 package เราก็จะสามารถสร้างไฟล์ที่มีนามสกุล .go ไว้ในนั้นได้ จะมีเพียงไฟล์เดียว หรือหลายไฟล์ก็ย่อมได้ด้วยเช่นกัน

และดูเหมือน rakyll จะค่อนข้างให้ความสำคัญกับเรื่องนี้เป็นอย่างมาก เธอเชื่อว่า ถ้าเราเข้าใจเรื่องของ package นี้ดีพอ เราจะสามารถสร้างโค้ด Go ที่มีประสิทธิภาพที่ดีขึ้นได้

การจัดการโครงสร้าง package

แยกไฟล์ออกเป็นหลายๆไฟล์

เนื่องจาก package ใน Go นั้นก็คือ director ที่สามารถมีไฟล์ได้หลายๆไฟล์อยู่ในนั้น เพราะฉะนั้น ก็ไม่ต้องลังเล หรือรู้สึกผิด ที่จะแยกโค้ดของเราออกเป็นไฟล์ต่างๆ เพื่อความเหมาะสม และจะทำให้โค้ดอ่านง่ายขึ้นด้วย

โดยเธอยกตัวอย่าง package ที่ผมเองก็ชอบมากเหมือนกัน นั่นก็คือ net/http โดย http จะแยกไฟล์ย่อยๆออกไปเพื่อจัดระเบียบเช่น ไฟล์ที่เกี่ยวกับ header type และโค้ดที่ทำงานกับมัน หรือ cookie type และโค้ดที่ทำงานเกี่ยวกับมัน แบบที่จะเห็นด้านล่างนี้

- doc.go       // สำหรับเป็น document ของ package นี้
- headers.go   // HTTP headers types and code
- cookies.go   // HTTP cookies types and code
- http.go      // HTTP client implementation, request and response types, etc.
Enter fullscreen mode

Exit fullscreen mode

ใช้แนวทาง ให้ type อยู่ใกล้มือที่สุด

มันคือหลักการง่ายๆ ที่ชาว Go มักจะทำกัน นั่นก็คือให้วาง type เอาไว้ให้ใกล้กับโค้ดที่ใช้มันให้มากที่สุด เพราะนี่มันจะเพิ่มความง่ายให้ใครก็ตามที่เข้ามาอ่านโค้ดขึ้นอีกโข มันดีมากจริงๆนะ ที่พอเราอ่านโค้ดอยู่แล้วเห็น type มันวางอยู่ใกล้ ทีนี้คุณลองเดาสิว่า ถ้าชาว Go จะวาง type ของ struct Header เขาจะเอามันไปวางไว้ที่ไหน

คำตอบคือ ก็สร้างไฟล์ชื่อ headers.go แล้วใส่มันลงไปในนั้นเลย

$ cat headers.go
package http

// Header represents an HTTP header.
type Header struct {...}
Enter fullscreen mode

Exit fullscreen mode

นอกจากว่าเราจะแยกไฟล์ของเรื่องนั้นออกไปแล้ว เราก็ยังชอบไปสร้าง type ของมันไว้บนสุดของไฟล์อีกด้วย แล้วถัดลงมา เราก็มักจะเขียนโค้ดที่ใช้ type นั้นต่อลงมาเลย (ความเห็นส่วนตัว ผมโคตรชอบ เพราะพวกผมเองก็ทำกันมาแบบนี้ตั้งแต่เริ่มเขียน Go เหมือนกัน)

สร้าง package ตามหน้าที่ของมัน

แนวทางปฏิบัติของภาษาอื่น อาจจะชอบจับเอา type มาวางไว้รวมกันแล้วตั้งชื่อว่า models หรือ types แต่ใน Go เราสร้าง package ตามหน้าที่ของมันครับ ดูตัวอย่างที่ rakyll หยิบมาแซวสิครับ

package models // DON'T DO IT!!!

// User represents a user in the system.
type User struct {...}
Enter fullscreen mode

Exit fullscreen mode

บอกตรงๆ ผมขำไม่ออกนะ เพราะเห็นทุกวัน มันปวดใจมากกว่าขำ

rakyll เสนอว่า แทนที่จะจับเอาทุก type มากองรวมกันแล้วให้ชื่อมันว่า models เราเปลี่ยนวิธีคิดกันให้เป็นแบบ Go เช่น type ของ User ก็ควรไปอยู่กับ package ที่มันจะถูกใช้เลยสิ

package mngtservice

// User represents a user in the system.
type User struct {...}

func UsersByQuery(ctx context.Context, q *Query) ([]*User, *Iterator, error)

func UserIDByEmail(ctx context.Context, email string) (int64, error)
Enter fullscreen mode

Exit fullscreen mode

แบบนี้ type User ก็ได้กลับบ้านไปอยู่ถูกที่ถูกทางซะที

ดู godoc เป็นแนวทางเพิ่มเติม

เรื่องนี้ผมเองก็ไม่เคยนึกถึงเหมือนกัน โดย rakyll เสนอว่าให้ลองรัน godoc ดูไปด้วยตั้งแต่ตอนเริ่มต้นเขียน API เลยยิ่งดี เพื่อดูว่าตอนที่มัน render เอกสารออกมาแล้วเป็นอย่างไร เพราะบางครั้งเวลาเราเห็น godoc เราอาจนึกอะไรออก และมันจะมีส่วนช่วยในการตัดสินใจเกี่ยวกับการวางโครงสร้าง หรือการตั้งชื่อต่างๆให้ดีขึ้นได้ด้วย และเพิ่มเติมคือ ถ้าสามารถเพิ่ม example เข้าไปด้วยได้จะยิ่งดีขึ้นไปอีกนะ

อย่า Export อะไรออกมาจาก main

เนื่องจาก Go ให้เราสามารถ export ของออกจาก package เพื่อให้ package อื่นมาเรียกใช้ได้ แต่การ export ของออกจาก main package มันไม่มีประโยชน์ นั่นหมายถึง เราก็ไม่ควรตั้งชื่ออะไรก็ตามให้ขึ้นต้นด้วย capital letter เพราะ main ควรเป็นผู้เรียกใช้ทุก package และถ้า main ดัน export ของออกไป แล้วใครจะเรียกกลับมาได้ก่อน ไม่งั้นก็เกิด import cycles ซึ่ง compiler ไม่ยอมอยู่แล้ว

มีข้อยกเว้นในกรณีที่เราจะทำพวก .so .a หรือ Go plugin ที่จำเป็นต้อง export ออกให้คนอื่นเรียกใช้ได้ซึ่งเป็นกรณีเฉพาะ

การตั้งชื่อ package

ชื่อ package กับ import path นั้นสำคัญมาก เพราะมันจะบอกให้รู้ว่าในนั้นมีอะไรอยู่ และถ้าชื่อมันดี ไม่ใช่แค่โค้ดเราจะมีคุณภาพดีเพียงอย่างเดียว แต่มันจะดีต่อคนที่เข้ามาดูแล หรือมาอ่านมันต่อจากเราด้วย

Lowercase only

ปรมาจารย์ Go ทุกคนย้ำแรงๆว่า ให้ตั้งชื่อด้วย lowercase อย่างเดียว อย่ามา snake_case หรือ camelCase ให้เห็นเด้อ แถมเธอยังโยนกลับไปให้ Sameer ที่ผมเล่าไปคราวที่แล้วอีกต่างหาก https://go.dev/blog/package-names ว่าให้ไปอ่านด้วย

ตั้งชื่อให้สั้น แต่สื่อให้ชัด

ชื่อ package ควรตั้งให้สั้นครับ แต่มันก็ต้องมีความเฉพาะจนสามารถระบุตัวตนของ package นั้นได้เลย ว่ามันคืออะไร เมื่อคนอื่นเห็นชื่อนั้น เขาควรจะเข้าใจคร่าวๆได้เลยว่า อ๋อเจ้า package นี้มันน่าจะทำอันนี้แน่ๆ

rakyll ยังเตือน แบบเดียวกับทุกสำนักอีกครั้งว่า อย่าตั้งชื่อที่มันกว้างเกินไปอย่าง “common” หรือ “util” (น้ำตาก็ไหลออกมาอีกละ 😭)

import "pkgs.org/common" // DON'T!!!
Enter fullscreen mode

Exit fullscreen mode

หลีกเลี่ยงการใช้ชื่อซ้ำ ลองคิดสภาพเวลาต้อง import ชื่อที่เหมือนกัน แต่อยู่คนละ path เข้ามาดู เราก็ต้องมาเดือดร้อนตั้ง alias ให้มัน แล้วความหมายมันก็อาจจะบิดๆไป ดูไม่งามเลย

rakyll บอกว่า ถ้าเราไม่สามารถหลีกเลี่ยงชื่อที่มันดูแย่ได้ นั่นเป็นไปได้สูงว่า เราอาจจะต้องกลับไปดูอีกทีว่าโครงสร้างที่เราอุตส่าห์ทำมันขึ้นมานั้น แล้วก็ภูมิใจกับมันมาก มันอาจจะมีอะไรผิดอยู่หรือเปล่า เราวางของผิดที่ผิดทางอยู่ไหม

ตรวจดูความสวยงามเวลา import ด้วย

บางทีเราอาจจะเคยชินกับการใช้ sub-directory บางอย่างจนมันไปโผล่อยู่บน import path แบบนี้


github.com/user/repo/src/httputil   // DON'T DO IT, AVOID SRC!!

github.com/user/repo/gosrc/httputil // DON'T DO IT, AVOID GOSRC!!

พยายามเลี่ยงรูปแบบนี้ไว้นะ

อย่าตั้งชื่อเป็น plurals

ชาว Go เราไม่ตั้งชื่อเป็น plurals กันนะ ซึ่งเอาจริงมันก็อาจจะทำให้คนที่เขียนภาษาอื่นบางภาษาที่อาจจะเคยชินกับการตั้งชื่อแบบนี้รู้สึกตะขิดตะขวงใจอยู่บ้าง เช่นว่า แทนที่จะตั้งชื่อ httputils ก็ให้ใช้ชื่อ httputil แทนไรงี้


package httputils  // DON'T DO IT, USE SINGULAR FORM!!

การตั้งชื่อใหม่ด้วย alias ก็ให้สร้างกฏมาใช้ร่วมกันซะ

บางครั้งเราก็อาจจะจำเป็นจะต้อง import ชื่อที่มันตั้งชื่อ package ตรงกันเป๊ะเข้ามา ทีนี้เราก็จะต้องเลือกเปลี่ยนชื่อมันสักตัวนึงทำ alias ให้มันไว้ ซึ่งตรงนี้มันไม่มีกฏอะไรตายตัว แต่ก็แนะนำว่า ถ้าจะเปลี่ยนชื่อพวก standard package การเติมคำว่า go เข้าไปข้างหน้าก็เป็นตัวเลือกที่ใช้ได้เช่น gourl หรือ goioutil อะไรแบบนี้ จะได้เข้าใจว่ามันเป็น package ที่มาจาก go standard lib นะ

import (
    gourl "net/url"

    "myother.com/url"
)
Enter fullscreen mode

Exit fullscreen mode

Package Documentation

ให้ทำ doc เอาไว้เสมอ โดยปกติมันจะอยู่ที่บรรทัดบนสุด เหนือคำประกาศ package แบบบรรทัดติดกันเลย แนวทางปฏิบัติคือ ถ้าไม่ใช่ main package มักจะ comment เริ่มด้วย “Package {pkgname}” ตามด้วยคำอธิบาย แต่ถ้าเป็น main package ก็ให้อธิบายว่า lin หรือ program นั้นทำอะไร

// Package ioutil implements some I/O utility functions.
package ioutil

// Command gops lists all the processes running on your system.
package main

// Sample helloworld demonstrates how to use x.
package main
Enter fullscreen mode

Exit fullscreen mode

ใช้ doc.go

บางครั้ง doc เราก็อยากจะเล่ายาวๆ เช่นอยากจะเล่าว่า package นี้มันคืออะไร มันดียังไง วิธีใช้ และอะไรต่อมิอะไร ซึ่งถ้ามันจะยาวเบอร์นี้ ก็ให้ย้ายไปไว้ในไฟล์ doc.go (ดูจากตัวอย่างได้ที่นี่ doc.go

ก็จบไปแล้วนะครับ กับแนวทางการใช้ Go Package โดย rakyll ซึ่งผมอาจจะตัดเนื้อหาบางส่วนออกเนื่องจากไม่สอดคล้องกับการใช้งานจริงในปัจจุบัน

Check out our other content

Check out other tags:

Most Popular Articles