Introduction
Go หรือที่หลายคนเรียกว่า Golang เป็นภาษาโปรแกรมแบบ Open Source ที่พัฒนาโดย Google ในปี 2009 โดยมีเป้าหมายเพื่อแก้ปัญหาที่ภาษาเดิม ๆ เริ่มตอบโจทย์ได้ไม่ดี ไม่ว่าจะเป็นเวลาในการ Compile ที่นาน ความซับซ้อนของภาษา หรือการพัฒนาโปรแกรมที่ต้องรองรับการทำงานพร้อมกัน (Concurrency) ในระดับสูง
Go ถูกออกแบบให้มี Syntax ที่เรียบง่าย อ่านง่าย และมีเครื่องมือมาตรฐานติดมากับภาษา ทำให้นักพัฒนาสามารถเริ่มสร้างโปรเจกต์ได้อย่างรวดเร็วโดยไม่ต้องเสียเวลาตั้งค่า Environment หรือค้นหาเครื่องมือเพิ่มเติมมากนัก นอกจากนี้ยังเป็นภาษาที่ Compile เป็น Native Binary จึงสามารถนำโปรแกรมไปรันได้โดยไม่ต้องติดตั้ง Runtime เพิ่มเติมในเครื่องปลายทาง
ปัจจุบัน Go ได้รับความนิยมอย่างมากในงาน Backend Development, Cloud Computing, Microservices, DevOps Tools และ Command Line Applications โดยมีบริษัทขนาดใหญ่จำนวนมากเลือกใช้ Go ในระบบ Production เช่น Google, Docker, Kubernetes, Cloudflare และ Grafana
Why Go?
หลายคนอาจสงสัยว่า ในเมื่อมีภาษาอย่าง Java, C++, Python หรือ Node.js อยู่แล้ว เหตุใด Google จึงต้องสร้างภาษาใหม่ขึ้นมาอีกภาษา
คำตอบคือ Google ต้องการภาษาที่สามารถพัฒนาโปรแกรมขนาดใหญ่ได้ง่าย แต่ยังคงมีประสิทธิภาพสูง ภาษาเดิมหลายภาษามักแลกมาด้วยความซับซ้อน เช่น C++ ที่มี Feature จำนวนมาก หรือภาษาที่ตีความ (Interpreted Language) ซึ่งอาจไม่ตอบโจทย์ด้านประสิทธิภาพและการใช้ทรัพยากร
Go จึงเลือกแนวทางตรงกันข้าม โดยลดจำนวน Keyword ให้เหลือน้อย ใช้ Syntax ที่สม่ำเสมอ และมีมาตรฐานเดียวกันสำหรับนักพัฒนาทุกคน ส่งผลให้โค้ดอ่านง่าย ดูแลรักษาได้สะดวก และลดความแตกต่างของ Coding Style ภายในทีม
Go เหมาะกับงานประเภทไหน
Go ถูกออกแบบมาสำหรับงานฝั่ง Server เป็นหลัก แต่สามารถนำไปใช้ได้หลากหลายประเภทเช่น
- REST API
- Backend Service
- Microservices
- CLI Tools
- Cloud Native
หากคุณอยากพัฒนา Web API, Backend Service หรือระบบที่ต้องรองรับผู้ใช้งานจำนวนมาก Go ถือเป็นหนึ่งในตัวเลือกที่ได้รับความนิยมที่สุดในปัจจุบัน
Popular Frameworks
แม้ Go จะสามารถสร้าง Web Server ได้ด้วย Standard Library (net/http) แต่ในงานจริง นักพัฒนาส่วนใหญ่มักเลือกใช้ Framework เพื่อช่วยลดโค้ดที่ต้องเขียนซ้ำ และเพิ่มความสะดวกในการพัฒนา
Your First Program
main.go ที่จะแสดงข้อความแสดงข้อความ "Hello, World!"
ทุกไฟล์ภาษา Go จะต้องเริ่มต้นด้วยการประกาศ Package
Package คือการจัดกลุ่มโค้ดที่มีหน้าที่เกี่ยวข้องกัน คล้ายกับ Module หรือ Namespace ในภาษาอื่น
หากใช้ชื่อ Package อื่น เช่น package math ไฟล์นั้นจะกลายเป็น Library ไม่ใช่โปรแกรมที่สามารถรันได้โดยตรง
Variables & Constants
การประกาศตัวแปรใน Go มีเพียงไม่กี่รูปแบบ แต่แต่ละรูปแบบมีจุดประสงค์ที่แตกต่างกันอย่างชัดเจน การเลือกใช้ var, := หรือ const ไม่ใช่แค่เรื่องของ Syntax แต่เป็นส่วนหนึ่งของ Idiomatic Go ที่ทำให้โค้ดอ่านง่ายและสื่อความหมายได้ดี
โดยทั่วไป โค้ด Go จะใช้ := เป็นค่าเริ่มต้นสำหรับตัวแปรภายในฟังก์ชัน ใช้ var เมื่อจำเป็นต้องระบุชนิดข้อมูลหรือประกาศไว้ก่อนใช้งาน และใช้ const สำหรับค่าที่ไม่ควรเปลี่ยนแปลงตลอดอายุของโปรแกรม
Variable Declaration
การประกาศตัวแปร
const ใช้สำหรับค่าที่ไม่เปลี่ยนแปลงตลอดอายุของโปรแกรม เช่น ชื่อแอปพลิเคชัน, Default Port หรือ HTTP Status ต่างจากตัวแปรตรงที่ค่าจะถูกกำหนดตั้งแต่ Compile Time ทำให้ Compiler สามารถนำค่าไป Optimize ได้ และหากมีการพยายามเปลี่ยนค่า จะเกิด Compile Error ทันที
ชื่อ AppName ขึ้นต้นด้วย ตัวพิมพ์ใหญ่ (Uppercase) จะถือเป็น Exported หมายถึงสามารถเข้าถึงได้จาก package อื่น (เดี๋ยวกลับมาอธิบายอีกที)
Zero Value
หนึ่งในแนวคิดที่ทำให้ Go แตกต่างจากหลายภาษา คือ ทุกตัวแปรต้องมีค่าเริ่มต้นเสมอ หากไม่ได้กำหนดค่า Go จะเลือก Zero Value ตามชนิดข้อมูลให้โดยอัตโนมัติ
- int = 0
- float64 = 0
- bool = false
- string = ""
- pointer = nil
- slice = nil
- map = nil
- interface = nil
แนวคิดนี้ช่วยลดโอกาสเกิดข้อผิดพลาดจากการใช้งานตัวแปรที่ยังไม่ได้กำหนดค่า และเป็นเหตุผลที่หลายโครงสร้างใน Go ไม่จำเป็นต้องมี Constructor เพียงเพื่อกำหนดค่าเริ่มต้น
สาเหตุที่ภาษา Go เรียกว่า Zero Value เพราะค่าเริ่มต้นของประเภทข้อมูลพื้นฐานทั้งหมดถูกแทนที่ด้วย "บิตที่เป็นศูนย์" (Zeroed Memory) ในระดับฮาร์ดแวร์เมื่อมีการประกาศตัวแปร
Data Types
Go เป็นภาษาแบบ Statically Typed ทุกค่าจะมีชนิดข้อมูล (Type) ที่แน่นอนตั้งแต่ Compile Time ช่วยให้ Compiler ตรวจสอบข้อผิดพลาดได้เร็วขึ้น และลดความผิดพลาดที่มักพบในภาษาแบบ Dynamic เช่น การนำ string ไปบวกกับ int หรือการเรียก Method ที่ไม่มีอยู่จริง
แม้ Go จะมีชนิดข้อมูลไม่มาก แต่ครอบคลุมการใช้งานส่วนใหญ่ และมีปรัชญาที่ชัดเจนคือ "เลือก Type ให้เหมาะกับข้อมูล" มากกว่าพยายามทำให้ Type ทำได้ทุกอย่าง
Basic Data Types
ประเภทข้อมูลพื้นฐาน
int เป็นชนิดข้อมูลสำหรับจำนวนเต็ม ขนาดของมันขึ้นอยู่กับสถาปัตยกรรมของเครื่อง (32 หรือ 64 บิต) หากไม่มีข้อกำหนดพิเศษ เช่น การรับส่งข้อมูลผ่าน Network หรือการ Mapping Database การใช้ int มักเป็นตัวเลือกที่เหมาะสมที่สุด
Array
Array คือชุดข้อมูลที่มีขนาดคงที่ และขนาดของ Array ถือเป็นส่วนหนึ่งของชนิดข้อมูล
[3]int หมายถึง Array ที่เก็บ int ได้ 3 ตัวพอดี ดังนั้นหากคุณส่ง [3]int เข้าไปในฟังก์ชันที่รับ [5]int จะไม่สามารถทำได้เพราะถือเป็นคนละ Type
Slice
Slice เป็น Data Structure ที่ใช้งานบ่อยที่สุดใน Go และสามารถมองได้ว่าเป็น "หน้าต่าง" ที่อ้างอิงข้อมูลใน Array อีกทีหนึ่ง เหมาะสำหรับ Collection ทั่วไป
[]string คือ Slice ของ string ซึ่งไม่มีการกำหนดขนาดล่วงหน้า
Map
Map ใช้สำหรับเก็บข้อมูลในรูปแบบ Key-Value และถูก Optimize สำหรับการค้นหาข้อมูลด้วย Key
map[string]string หมายถึง Key และ Value เป็น string
Go มีชนิดข้อมูลไม่มาก แต่แต่ละชนิดมีหน้าที่ชัดเจน Array เหมาะกับข้อมูลขนาดคงที่ Slice เป็น Collection หลักของภาษา ส่วน Map ใช้สำหรับการค้นหาข้อมูลแบบ Key-Value การเลือกใช้ชนิดข้อมูลให้เหมาะสมตั้งแต่แรก จะช่วยให้โค้ดอ่านง่าย มีประสิทธิภาพ และสอดคล้องกับแนวทางการเขียน Go ในโปรเจกต์ Production
Control Flow
Control Flow ของ Go มี Keyword ไม่มาก แต่ครอบคลุมการทำงานเกือบทั้งหมดของภาษา Go ไม่มี while หรือ do...while และ switch ไม่จำเป็นต้องใช้ break เหมือน C หรือ Java แนวคิดคือทำให้ภาษาเล็กลง แต่ยังคงอ่านง่ายและลดโอกาสเกิดข้อผิดพลาด
if
Go อนุญาตให้ประกาศตัวแปรภายใน if ได้ ทำให้ Scope ของตัวแปรถูกจำกัดอยู่เฉพาะเงื่อนไขนั้น ลดการสร้างตัวแปรที่ไม่จำเป็นใน Scope ภายนอก
grade := score / 10 ถูกประกาศภายใน if และจะใช้งานได้เฉพาะใน if, else if และ else ชุดเดียวกัน
switch
switch ของ Go มีความสามารถมากกว่าหลายภาษา โดยจะหยุดทำงานหลังจบแต่ละ case อัตโนมัติ จึงไม่ต้องเขียน break
แต่ละ case จะจบการทำงานทันทีเมื่อ Match จึงไม่มีปัญหา Fallthrough โดยไม่ตั้งใจ
for
Go มี Loop เพียงชนิดเดียวคือ for แต่สามารถใช้แทน while และ Infinite Loop ได้ทั้งหมด
รูปแบบนี้เป็น Standard for Loop ที่คุ้นเคยจากภาษาอื่น
range
range เป็นวิธีมาตรฐานในการวนลูปผ่าน Collection เช่น Slice, Array, Map และ String
range จะคืนค่าได้สองค่า คือ Index และ Value
break และ continue
continue ข้ามรอบปัจจุบันและเริ่มรอบถัดไปทันที
break ออกจาก Loop ทันที
ทั้งสอง Keyword ใช้ได้กับ for, switch และ select
Go ลดจำนวน Control Flow ลงเหลือเพียงไม่กี่ Keyword แต่ยังครอบคลุมการใช้งานทั้งหมด if รองรับการประกาศตัวแปรใน Scope ของเงื่อนไข switch ไม่ต้องใช้ break และ for ถูกออกแบบให้แทนทั้ง for, while และ Infinite Loop ได้ ส่วน range ใช้ในการวนลูปผ่าน Collection
Functions
Function เป็นหน่วยพื้นฐานของการจัดระเบียบโปรแกรมใน Go ทุกการทำงานไม่ว่าจะเป็นการคำนวณ การแปลงข้อมูล หรือการเรียกใช้งานส่วนอื่นของโปรแกรม ล้วนเกิดขึ้นผ่าน Function ภาษา Go พยายามทำให้ Function มีรูปแบบที่เรียบง่าย อ่านง่าย และคาดเดาได้ โดยตัดความสามารถบางอย่างที่พบในภาษาอื่นออก เช่น Function Overloading หรือ Default Parameters
แม้ Syntax ของ Function จะไม่ซับซ้อน แต่ Go เพิ่มความสามารถที่สำคัญเข้ามา เช่น Multiple Return Values และ Variadic Parameters ซึ่งช่วยให้สามารถออกแบบ API ที่ชัดเจนโดยไม่ต้องเพิ่ม Syntax ใหม่ของภาษา ในบทนี้เราจะทำความเข้าใจแนวคิดเหล่านี้ทีละส่วน
Declaration & Parameters
การประกาศ Function ใน Go มีรูปแบบที่คงที่ ทุก Function จะระบุชื่อ รายการ Parameter และชนิดข้อมูลของค่าที่คืนกลับไว้อย่างชัดเจน ทำให้สามารถเข้าใจหน้าที่ของ Function ได้จาก Signature เพียงอย่างเดียว
Function ทุกตัวเริ่มต้นด้วย Keyword func ตามด้วยชื่อ Function, Parameter และ Return type กลับ โดย Go ใช้รูปแบบ name type สำหรับ Parameter เช่น name string แทนที่จะเป็น string name
Return Values
ทุก Function ใน Go สามารถคืนค่าได้ตั้งแต่ศูนย์ค่าขึ้นไป ชนิดข้อมูลของค่าที่คืนจะเป็นส่วนหนึ่งของ Signature ทำให้ผู้เรียกทราบได้ทันทีว่า Function จะส่งข้อมูลประเภทใดกลับมา
คำสั่ง return ใช้สำหรับสิ้นสุดการทำงานของ Function และส่งค่ากลับไปยังผู้เรียก ชนิดข้อมูลของค่าที่คืนจะต้องตรงกับ Return Type ที่ประกาศไว้ หากคืนค่าผิดชนิด หรือไม่คืนค่าตามที่กำหนด โปรแกรมจะไม่สามารถ Compile ได้
การระบุ Return Type ไว้ใน Signature ทำให้สามารถเข้าใจผลลัพธ์ของ Function ได้โดยไม่ต้องเปิดดูรายละเอียดภายใน
Multiple Return Values
หนึ่งในความสามารถของ Go คือ Function สามารถคืนค่าได้มากกว่าหนึ่งค่า แนวคิดนี้ช่วยให้สามารถส่งผลลัพธ์พร้อมข้อมูลเพิ่มเติมกลับไปยังผู้เรียกได้ในครั้งเดียว
Return Type หลายค่าจะถูกประกาศไว้ภายในวงเล็บ โดยค่าจะถูกส่งกลับตามลำดับที่ประกาศไว้ ในตัวอย่างนี้ Function คืนทั้งผลลัพธ์ของการหารและค่า bool เพื่อบอกว่าการคำนวณสำเร็จหรือไม่
รูปแบบนี้ช่วยลดความจำเป็นในการสร้าง Struct สำหรับข้อมูลขนาดเล็ก และเป็นหนึ่งในแนวคิดสำคัญที่ถูกนำไปใช้ทั่วทั้งภาษา
Variadic Functions
บาง Function จำเป็นต้องรองรับ Argument จำนวนไม่แน่นอน Go จึงรองรับ Variadic Parameter ผ่านเครื่องหมาย ... ซึ่งช่วยให้ Function เดียวสามารถรับข้อมูลได้หลายรูปแบบ
เครื่องหมาย ... ใช้ประกาศ Variadic Parameter ซึ่งหมายถึง Parameter ที่สามารถรับ Argument ได้ตั้งแต่ศูนย์ค่าขึ้นไป แม้ว่าตอนเรียกใช้งานจะส่งจำนวน Argument แตกต่างกัน แต่ภายใน Function ตัวแปร numbers จะมีชนิดข้อมูลเป็น []int โดยอัตโนมัติ
ด้วยเหตุนี้ Variadic Parameter จึงสามารถใช้งานร่วมกับ len(), range หรือส่งต่อให้ Function ที่รับ Slice ได้เหมือน Slice ทั่วไป
Passing a Slice
หากข้อมูลอยู่ใน Slice อยู่แล้ว สามารถใช้ ... เพื่อกระจายสมาชิกของ Slice ให้กลายเป็นรายการ Argument ได้
Slice และ Variadic Parameter เป็นคนละแนวคิด แม้ภายใน Function Variadic Parameter จะเป็น Slice แต่เมื่อต้องการส่ง Slice เข้าไปยัง Variadic Function จะต้องใช้ ... เพื่อกระจายสมาชิกของ Slice ก่อน มิฉะนั้นชนิดข้อมูลจะไม่ตรงกับ Parameter ที่ประกาศไว้
Function ใน Go ถูกออกแบบให้มีรูปแบบที่เรียบง่ายและสม่ำเสมอ Signature ของ Function จะอธิบายข้อมูลที่รับเข้าและข้อมูลที่ส่งกลับอย่างชัดเจน ขณะที่ Multiple Return Values และ Variadic Parameters ช่วยเพิ่มความยืดหยุ่นโดยไม่เพิ่มความซับซ้อนของภาษา การเข้าใจแนวคิดเหล่านี้จะเป็นพื้นฐานสำคัญสำหรับการเรียนรู้การจัดการ Error และการออกแบบ API ในบทถัดไป
Struct
Struct เป็นชนิดข้อมูลที่ใช้สำหรับรวมข้อมูลหลายค่าเข้าด้วยกันภายใต้ชนิดข้อมูลเดียว หากตัวแปรพื้นฐานอย่าง int, string หรือ bool ใช้แทนข้อมูลเพียงค่าเดียว Struct จะใช้แทนข้อมูลที่ประกอบด้วยหลาย Field ที่มีความสัมพันธ์กัน เช่น ผู้ใช้ สินค้า หรือคำสั่งซื้อ
Go ไม่มี Class และไม่มี Inheritance แบบภาษาเชิงวัตถุหลายภาษา แต่เลือกใช้ Struct ร่วมกับ Method และ Composition เพื่อสร้างชนิดข้อมูลที่สามารถเก็บข้อมูลและกำหนดพฤติกรรมได้อย่างเรียบง่าย
Defining a Struct
Struct ใช้ Keyword type ร่วมกับ struct เพื่อสร้างชนิดข้อมูลใหม่ โดยแต่ละ Field จะมีชื่อและชนิดข้อมูลของตัวเอง
คำสั่ง type ใช้สำหรับสร้างชนิดข้อมูลใหม่ ส่วน struct ระบุว่าชนิดข้อมูลนั้นประกอบด้วยหลาย Field ที่ถูกจัดเก็บรวมกัน แต่ละ Field มีชนิดข้อมูลของตัวเอง และจะถูกจัดสรรอยู่ภายใน Object เดียวกัน
เมื่อสร้าง Struct แล้ว User จะกลายเป็นชนิดข้อมูลใหม่ของภาษา สามารถใช้ประกาศตัวแปร ส่งเข้า Function หรือคืนค่าจาก Function ได้เช่นเดียวกับชนิดข้อมูลพื้นฐาน
Methods
แม้ Go จะไม่มี Class แต่สามารถกำหนด Function ให้เป็นส่วนหนึ่งของชนิดข้อมูลได้ผ่าน Method Receiver
Receiver คือส่วนที่อยู่ระหว่าง Keyword func กับชื่อ Method เช่น (u User) ซึ่งระบุว่า Method นี้เป็นของชนิดข้อมูล User
Pointer Receiver
Receiver สามารถรับได้ทั้งค่า (Value Receiver) และ Pointer (Pointer Receiver) โดยแต่ละรูปแบบมีพฤติกรรมที่แตกต่างกัน
การใช้ *User หมายความว่า Receiver เป็น Pointer ไปยัง Struct เดิม ดังนั้นการแก้ไข u.Name จะกระทบกับข้อมูลต้นฉบับโดยตรง
Composition
Go ไม่มี Inheritance แต่สามารถประกอบ Struct จาก Struct อื่นได้ผ่าน Composition
เมื่อฝัง Struct หนึ่งไว้ภายในอีก Struct โดยไม่ตั้งชื่อ Field จะเรียกว่า Embedded Field ทำให้ Field และ Method ของ Struct ที่ถูกฝังสามารถเข้าถึงได้โดยตรง
Struct เป็นชนิดข้อมูลสำหรับรวมข้อมูลหลายค่าเข้าด้วยกัน และเป็นรากฐานของการสร้างชนิดข้อมูลใน Go เมื่อใช้งานร่วมกับ Method จะสามารถกำหนดพฤติกรรมให้กับข้อมูลได้ ส่วน Composition ช่วยให้ Struct หลายตัวทำงานร่วมกันได้โดยไม่ต้องพึ่ง Inheritance ซึ่งเป็นหนึ่งในแนวคิดสำคัญของการออกแบบภาษา Go
Pointer
Pointer เป็นชนิดข้อมูลที่ใช้เก็บ ตำแหน่งของข้อมูลในหน่วยความจำ (Memory Address) แทนที่จะเก็บค่าของข้อมูลโดยตรง แม้หลายคนจะมองว่า Pointer เป็นเรื่องยาก แต่ Go ได้ลดความซับซ้อนลงอย่างมาก โดยตัดความสามารถอย่าง Pointer Arithmetic ออกไป ทำให้ Pointer มีหน้าที่หลักเพียงการอ้างอิงข้อมูลเดิม
การเข้าใจ Pointer เป็นสิ่งสำคัญ เพราะ Go ใช้แนวคิดนี้ในหลายส่วนของภาษา เช่น Method Receiver, Function Parameters และการจัดการข้อมูลขนาดใหญ่ การรู้ว่าเมื่อใดควรใช้ Value และเมื่อใดควรใช้ Pointer จะช่วยให้เข้าใจพฤติกรรมของโปรแกรมได้ถูกต้อง
Getting a Pointer
เครื่องหมาย & ใช้สำหรับสร้าง Pointer จากตัวแปร โดยจะคืนตำแหน่งของข้อมูลในหน่วยความจำแทนค่าของข้อมูลนั้น
Operator & ใช้สำหรับอ้างอิงตำแหน่งของตัวแปร และคืนค่าเป็น Pointer ที่ชี้ไปยังข้อมูลนั้น
Dereferencing
เมื่อมี Pointer แล้ว สามารถใช้เครื่องหมาย * เพื่อเข้าถึงค่าที่ Pointer กำลังอ้างอิงอยู่
เครื่องหมาย * มีหน้าที่ตรงข้ามกับ &
& สร้าง Pointer จากข้อมูล
* อ่านค่าที่ Pointer กำลังอ้างอิง
กระบวนการอ่านค่าผ่าน Pointer เรียกว่า Dereferencing หาก Pointer ชี้ไปยังตัวแปร name การใช้ *ptr จะได้ค่าของ name กลับมา
Modifying Through a Pointer
Pointer ไม่ได้ใช้เพียงสำหรับอ่านข้อมูล แต่ยังสามารถใช้แก้ไขข้อมูลต้นฉบับได้
เมื่อกำหนดค่าให้ *ptr จะเป็นการแก้ไขข้อมูลที่ Pointer กำลังอ้างอิงอยู่ ไม่ใช่การเปลี่ยนแปลง Pointer เอง
Value vs Pointer
การส่งข้อมูลเข้า Function สามารถส่งได้ทั้งแบบ Value และ Pointer ซึ่งให้ผลลัพธ์ต่างกัน
เมื่อส่ง Struct แบบ Value เข้า Function Go จะสร้างสำเนาของข้อมูลขึ้นมา การเปลี่ยนแปลงภายใน Function จึงเกิดขึ้นกับสำเนาเท่านั้น และไม่ส่งผลต่อข้อมูลต้นฉบับ
nil Pointer
Pointer สามารถไม่มีข้อมูลที่อ้างอิงได้ โดยจะมีค่าเป็น nil
เมื่อประกาศ Pointer แต่ยังไม่ได้กำหนดให้ชี้ไปยังข้อมูลใด Go จะกำหนด Zero Value ของ Pointer เป็น nil
Pointer เป็นชนิดข้อมูลที่ใช้อ้างอิงตำแหน่งของข้อมูลในหน่วยความจำ แทนที่จะเก็บค่าของข้อมูลโดยตรง การใช้ & และ * ทำให้สามารถสร้าง Pointer อ่านค่า และแก้ไขข้อมูลต้นฉบับได้ การเข้าใจความแตกต่างระหว่างการส่งข้อมูลแบบ Value และ Pointer เป็นพื้นฐานสำคัญสำหรับการเรียนรู้ Method Receiver และ Interface ใน Go
Interface
Interface เป็นชนิดข้อมูลที่ใช้กำหนด พฤติกรรม (Behavior) แทนที่จะกำหนดโครงสร้างของข้อมูล ต่างจาก Struct ที่อธิบายว่าข้อมูลมีอะไรบ้าง Interface จะอธิบายว่าชนิดข้อมูลนั้น "ทำอะไรได้บ้าง" ผ่านรายการของ Method
สิ่งที่ทำให้ Interface ของ Go แตกต่างจากหลายภาษาคือ ไม่ต้องประกาศว่ากำลัง Implement Interface หากชนิดข้อมูลใดมี Method ครบตามที่ Interface กำหนด Go จะถือว่า Type นั้น Implement Interface โดยอัตโนมัติ แนวคิดนี้เรียกว่า Implicit Implementation ซึ่งช่วยลดการเชื่อมโยงระหว่าง Type และทำให้โค้ดมีความยืดหยุ่นมากขึ้น
Defining an Interface
Interface ใช้ Keyword interface เพื่อกำหนดชุดของ Method โดยไม่ต้องระบุรายละเอียดการทำงานของแต่ละ Method
Interface เป็นการกำหนด "สัญญา" (Contract) ว่า Type ที่จะใช้งานร่วมกับ Interface นี้ต้องมี Method อะไรบ้าง ในตัวอย่าง Speaker กำหนดไว้เพียง Method เดียวคือ Speak() string
Interface ไม่ได้เก็บข้อมูล และไม่มี Implementation ของ Method หน้าที่ของมันคือระบุรูปแบบของพฤติกรรมที่ Type ต่าง ๆ ต้องมี
Implicit Implementation
Go ไม่ใช้ Keyword อย่าง implements หรือ extends ในการเชื่อมโยง Type กับ Interface
Function นี้ไม่ได้สนใจว่าค่าที่รับเข้ามาเป็น Dog หรือ Cat สิ่งเดียวที่สนใจคือค่าดังกล่าวสามารถทำตามสัญญาของ Speaker ได้หรือไม่
Empty Interface
Interface ที่ไม่มี Method เลยสามารถเก็บค่าของชนิดข้อมูลใดก็ได้
ตั้งแต่ Go 1.18 เป็นต้นมา ภาษาได้เพิ่ม Alias ชื่อ any ซึ่งมีความหมายเหมือนกับ interface{} ทุกประการ
Interface เป็นกลไกสำหรับกำหนดพฤติกรรมของชนิดข้อมูลผ่านชุดของ Method โดยไม่ผูกติดกับ Struct หรือ Type ใดโดยเฉพาะ การ Implement Interface ใน Go เกิดขึ้นโดยอัตโนมัติเมื่อ Type มี Method ตรงตามที่กำหนด ทำให้โค้ดมีความยืดหยุ่นและลดการพึ่งพาระหว่างส่วนต่าง ๆ ของระบบ ส่วน any หรือ interface{} เป็น Interface พิเศษที่สามารถเก็บค่าของชนิดข้อมูลใดก็ได้
Error Handling
การจัดการข้อผิดพลาด (Error Handling) เป็นหนึ่งในแนวคิดที่แตกต่างที่สุดของ Go แทนที่จะใช้ Exception เหมือนหลายภาษา Go เลือกให้ข้อผิดพลาดเป็น "ค่าชนิดหนึ่ง" (error) ที่สามารถส่งกลับจาก Function และตรวจสอบได้เหมือนข้อมูลทั่วไป
แนวคิดนี้ทำให้ Flow ของโปรแกรมชัดเจนขึ้น เพราะทุกจุดที่อาจเกิดข้อผิดพลาดจะถูกระบุไว้ใน Signature ของ Function ผู้เรียกจึงทราบตั้งแต่ต้นว่าจะต้องตรวจสอบ Error ก่อนนำผลลัพธ์ไปใช้งาน
The error Type
ใน Go error เป็น Interface ที่ถูกกำหนดไว้ใน Standard Library โดย Function ที่อาจเกิดข้อผิดพลาดมักจะคืน error เป็นค่าตัวสุดท้าย
เมื่อ Function อาจล้มเหลว Convention ของ Go คือคืน error เป็นค่าตัวสุดท้าย หากการทำงานสำเร็จจะคืน nil ซึ่งหมายถึงไม่มีข้อผิดพลาด แต่หากเกิดปัญหา จะคืน Error Object ที่อธิบายสาเหตุของความผิดพลาด
รูปแบบนี้ทำให้ผู้เรียกทราบทันทีจาก Signature ว่าจำเป็นต้องตรวจสอบ Error ก่อนใช้งานค่าที่คืนกลับ
Creating Errors with errors.New
เมื่อต้องการสร้าง Error ที่มีข้อความคงที่ สามารถใช้ errors.New()
Function errors.New() ใช้สร้าง Error ใหม่พร้อมกับข้อความที่ต้องการ
Formatting Errors with fmt.Errorf
หากข้อความ Error ต้องประกอบด้วยค่าของตัวแปร สามารถใช้ fmt.Errorf()
fmt.Errorf() ทำงานคล้าย fmt.Sprintf() แต่แทนที่จะคืน string จะสามารถคืนค่าของ error ได้
Returning Errors
เมื่อ Function เรียก Function อื่นที่คืน Error มา ควรตรวจสอบและส่ง Error กลับทันทีหากไม่สามารถจัดการได้
Error สามารถส่งต่อระหว่าง Function ได้โดยตรง เพราะ error เป็น Interface ชนิดหนึ่ง ไม่จำเป็นต้องสร้าง Error ใหม่ทุกครั้ง
Go มองข้อผิดพลาดเป็นข้อมูลชนิดหนึ่ง ไม่ใช่เหตุการณ์พิเศษของภาษา Function ที่อาจล้มเหลวมักคืน error เป็นค่าตัวสุดท้าย และผู้เรียกมีหน้าที่ตรวจสอบก่อนใช้งานผลลัพธ์ errors.New() เหมาะสำหรับข้อความคงที่ ส่วน fmt.Errorf() ใช้สร้าง Error ที่มีรายละเอียดจากค่าของตัวแปร แนวทางนี้ทำให้การจัดการข้อผิดพลาดมีความชัดเจนและสามารถติดตาม Flow ของโปรแกรมได้ง่าย
Packages & Modules
เมื่อโปรแกรมเริ่มมีขนาดใหญ่ การเขียนทุกอย่างไว้ในไฟล์เดียวจะทำให้ดูแลรักษาได้ยาก Go จึงแบ่งการจัดการโค้ดออกเป็นสองระดับ ได้แก่ Package สำหรับจัดกลุ่ม Source Code ที่เกี่ยวข้อง และ Module สำหรับจัดการโปรเจกต์และ Dependency
Package เป็นกลไกสำหรับจัดระเบียบโค้ดภายในโปรเจกต์ ส่วน Module เป็นกลไกระดับโปรเจกต์ที่ใช้กำหนดชื่อโมดูล เวอร์ชัน และ Dependency ทั้งหมด ทั้งสองแนวคิดทำงานร่วมกันและเป็นพื้นฐานของทุกโปรเจกต์ Go
Package
ทุกไฟล์ .go จะต้องประกาศว่าอยู่ใน Package ใดผ่าน Keyword package
Package คือการรวม Function, Struct, Interface และตัวแปรที่เกี่ยวข้องไว้ในกลุ่มเดียวกัน ทุกไฟล์ภายในโฟลเดอร์เดียวกันจะต้องประกาศ Package เดียวกัน เพื่อให้ Compiler มองว่าเป็นส่วนหนึ่งของหน่วยเดียวกัน
Package main เป็น Package พิเศษที่ใช้สำหรับสร้าง Executable Program และต้องมี Function main() เป็นจุดเริ่มต้นของโปรแกรม
Exported Identifiers
Go ใช้ตัวอักษรตัวแรกของชื่อเพื่อกำหนดระดับการเข้าถึง (Visibility)
Identifier ที่ขึ้นต้นด้วย ตัวพิมพ์ใหญ่ (Uppercase) จะถูก Export และสามารถเรียกใช้งานจาก Package อื่นได้
ในตัวอย่าง Add() สามารถเรียกผ่าน math.Add() ได้ เพราะชื่อ Function เริ่มต้นด้วยตัวอักษรใหญ่
Go Modules
Go Module ใช้สำหรับกำหนดชื่อโปรเจกต์และจัดการ Dependency
คำสั่ง go mod init ใช้สร้าง Module ใหม่และสร้างไฟล์ go.mod ขึ้นมา โดยชื่อ Module มักใช้เป็น Repository Path เช่น github.com/user/project
หลังจากรันคำสั่ง จะได้ไฟล์
หลังจากสร้าง Module แล้ว ทุก Package ภายในโปรเจกต์จะสามารถ Import กันเองผ่านชื่อ Module ได้ และ Go จะใช้ข้อมูลใน go.mod เพื่อจัดการ Dependency ทั้งหมดของโปรเจกต์
Importing External Packages
เมื่อต้องใช้งาน Library ภายนอก Go จะดาวน์โหลดและบันทึก Dependency ลงใน Module โดยอัตโนมัติ
เมื่อลอง Build หรือ Run โปรแกรม
go run .
Go จะเพิ่ม Dependency ลงใน go.mod และสร้างไฟล์ go.sum หากยังไม่มี
Code Explanation
เมื่อมีการ Import Package ที่ยังไม่ได้ติดตั้ง Go จะดาวน์โหลด Dependency ที่จำเป็นและบันทึกเวอร์ชันไว้ใน go.mod
พร้อมกันนั้น Go จะสร้างไฟล์ go.sum เพื่อเก็บ Checksum ของ Dependency ที่ดาวน์โหลดมา ช่วยตรวจสอบความถูกต้องของ Package และทำให้ทุกคนในโปรเจกต์ใช้ Dependency เวอร์ชันเดียวกัน
Package ใช้สำหรับจัดระเบียบโค้ดภายในโปรเจกต์ ขณะที่ Module ใช้สำหรับจัดการโปรเจกต์และ Dependency ทั้งหมด การเข้าใจการแบ่ง Package การ Export Identifier และการทำงานของ go.mod จะช่วยให้สามารถจัดโครงสร้างโปรเจกต์ Go ได้อย่างถูกต้อง และเป็นพื้นฐานก่อนเริ่มพัฒนา Web API ด้วย Framework อย่าง Gin
ตอนต่อไป
Building REST APIs with Gin
coming soon...