Golang 101: พื้นฐานโกฉบับโปรแกรมเมอร์
ภาษาโกคือ?
ภาษา Go (หรือเรียกว่า Golang) ถูกสร้างโดย Google เป้าหมายคือภาษาสำหรับเขียนโปรแกรมแนว System Programming การเขียนโปรแกรมในฝั่ง Backend เช่นการสร้าง API Server หรือ Network Application
จุดเด่นของภาษาโกคือ
Compiler Language
เป็นภาษาแบบ Compiler คือมีการแปล Source Code ทั้งหมดให้กลายเป็น Executable File (ภาษาเครื่อง) ซึ่งคอมพิวเตอร์สามารถนำไปรันได้ทันที ไม่ต้องแปลคำสั่งใหม่ทุกรอบแบบภาษาแนว Script เช่น Node.js หรือ PHP ทำให้ทำงานได้เร็วส์!
Static Type
โกเป็นภาษาแบบ Static-Type คือตัวแปรต้องกำหนดชนิดตั้งแต่แรก ทำให้คอมไพเลอร์ช่วยเราเช็กข้อผิดพลาดได้ตั้งแต่ตอนเขียนโปรแกรมเลย ถ้าใครเคยเขียนโปรแกรมขนาดใหญ่ด้วยภาษาแนว Dynamic-Type เช่น Python, JavaScript, PHP น่าจะรู้ความปวดหัวและบั๊กของการใช้ตัวแปรแบบไม่มีการกำหนดชนิด (ภายหลังมีการสร้าง TypeScript มาใช้แทน JavaScript, ส่วน PHP ตั้งแต่เวอร์ชัน 7 ก็ถูกเพิ่ม Type ให้ตัวแปรแล้ว ... เหลือแค่ Python นี่แหละ)
ลักษณะของภาษา
- ใช้เวลาคอมไพล์น้อย ได้งานเร็ว เขียนโค้ดแล้วไม่ต้องมานั่งรอนานๆ
- ออกแบบมาสำหรับเขียนโปรแกรมแบบ Parallel โดยเฉพาะ
- Syntax ของภาษาถูกออกแบบให้ใช้งานง่ายมากๆ โดยใช้คอนเซ็ปตามฉบับของภาษา C (แต่ไม่ต้องใส่
;
) ไม่ค่อยมีคำสั่งพิเศษๆ ทำให้กรณีทำงานเป็นทีม แต่ละคนจะเขียนโค้ดออกมาคล้ายๆ กัน
Garbage Collector
ภาษาโกมีการใช้ Garbage Collector สำหรับเคลียร์ตัวแปรหรืออ็อบเจคที่ไม่ถูกใช้งานแล้วออกจากหน่วยความจำให้โดยโปรแกรมเมอร์ไม่ต้องจัดการเอง
สามารถดาวน์โหลดคอมไพเลอร์ภาษาโกได้ที่ https://golang.org
Hello Go!
มาเริ่มเขียนโกกัน สร้างไฟล์ main.go
ขึ้นมาก่อน แล้วเขียนโค้ดนี้ลงไป
package main
import "fmt"
func main() {
fmt.Println("Hello Go ~")
}
หลังจากนั้นสามารถสั่งรันโปรแกรมได้ด้วยคำสั่ง
go run main.go
ซึ่งเราจะได้ผลลัพธ์เป็นคำว่า Hello Go ~ ออกมา
หากใครเคยเขียนภาษาตระกูล C มาก่อนน่าจะคุ้นๆ กับโค้ดชุดนี้ ภาษาโกจะเริ่มต้นทำงานที่ฟังก์ชันชื่อ main()
การ print ค่าออกมาใช้คำสั่ง Println()
จากแพกเกจชื่อ fmt
ย่อมาจาก formatted I/O
คำสั่งสำหรับการคอมไพล์ภาษาโกยังมีอีกมาก แต่เดี๋ยวเราจะพูดถึงเรื่องนี้กันต่อในบทต่อไป ในบทนี้ขอแสดงให้ดูภาพรวมของภาษาโกก่อนนะ
Variable ตัวแปร
ภาษาโกเป็น Static-Type ที่ต้องประกาศตัวแปรและกำหนดชนิดอย่างชัดเจนก่อนใช้เสมอ
โดยจะใช้คีย์เวิร์ด var
ตามด้วยชื่อตัวแปร และ Type
var <varname> <vartype>
เช่น
var num int
num = 7
var message string
message = "Expecto Patronum!"
ตำแหน่งของชนิดตัวแปรจะสลับกับภาษาตระกูล C ที่ปกติจะเอา Type ขึ้นก่อน เนื่องจากโกอยากให้ลักษณะการอ่านเป็นแบบมนุษย์ เช่น var num int
ก็จะอ่านได้ว่า สร้างตัวแปร num เป็นชนิด int
Basic Type
ชนิดตัวแปรในโกมีเหมือนๆ กับภาษาอื่นๆ แต่โกจะเน้นการตั้งชื่อชนิดตัวแปรโดยระบุชัดๆ ไปเลยว่าเรากำลังใช้ตัวแปรขนาดกี่ bit อยู่ (ไม่มีการแยก int
/long
, float
/double
, char
/string
)
bool
string
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
float32 float64
complex64 complex128
ที่สำคัญมากๆ คือสำหรับโก ชนิดตัวแปร primitive (ชนิดตัวแปรพื้นฐาน) นั้น ไม่สามารถเป็น nil
(หรือ null
ในภาษาอื่น) ดังนั้นตั้งแต่สร้างตัวแปรขึ้นมา ถ้าเราไม่ได้กำหนดค่าให้มัน มันจะมีการกำหนด default value ให้เองเลย
Type | Zero Value |
---|---|
int, uint, float | 0 |
complex | (0+0i) |
string |
"" (empty string) |
bool | false |
function | nil |
var x int = 0
var y int
//x = 0, y = 0 too
Type Inference
เป็นรูปแบบย่อสำหรับการสร้างตัวแปร โดยให้คอมไพล์เลยกำหนดไทป์ให้เราแทนจาก value ที่กำหนดไว้
<variable> := <value>
ข้อสังเกตของการประกาศตัวแปรแบบนี้คือไม่ต้องใส่คำว่า var
แล้วนะ แค่เปลี่ยน =
เป็น :=
ก็พอ
num := 7
platform := 9.75
message := "Expecto Patronum!"
//มีค่าเท่ากับ
var num int = 7
var platform float64 = 9.75
var message string = "Expecto Patronum!"
สำหรับการประกาศตัวแปรแบบ int และ float นั้นจะถูกกำหนดเป็น int
และ float64
ทันที ถ้าอยากได้ตัวแปรขนาดอื่นจะใช้การสร้างตัวแปรแบบนี้ไม่ได้ ต้องกำหนดเอง
Output
คำสั่ง I/O ขอโกเรียกว่าลอกมาจาก C เลย
fmt.Print("Hello Go ~")
fmt.Println("Hello Go ~") //print with new line
house := "Ravenclaw"
point := 10
fmt.Printf("%v points to %v", point, house)
//output: 10 points to Ravenclaw
สำหรับ format การแสดงผลแบบมาตราฐานใช้ %v
สำหรับ value ทั่วไป
แต่ก็สามารถใช้ฟอร์แมทพวก %d
, %f
เพื่อจัดรูปแบบการแสดงผลตัวเลขที่ละเอียดขึ้นได้เช่นกัน
ส่วนฟอร์แมทอื่นๆ สามารถดูได้ที่ https://golang.org/pkg/fmt/
String Interpolation
หากต้องการผสมค่าจากตัวแปรเข้ากันเป็น string สำหรับภาษาอื่นๆ ในยุคนี้ส่วนใหญ่จะใช้ $
ในการบอกว่าตำแหน่งนั้นเป็นตัวแปร
var year = 2020
var month = 12
var date = 31
var s = "$year / $month / $date"
แต่สำหรับโกไม่มีอะไรแบบนั้น แต่จะใช้ฟังก์ชัน sprintf
จากภาษา C (อีกแล้ว) ซึ่งทำงานคล้ายๆ กับ printf
แต่รีเทิร์นค่ากลับมาเป็น string แทนการปริ๊นออกมา
year := 2020
month := 12
date := 31
s := fmt.Sprintf("%v / %v / %v", year, month, date)
Flow Control
คำสั่งควบคุมในภาษาโกนั้นเรียบง่ายมาก คือเหมือนภาษา C แต่ไม่จำเป็นต้องเขียน ()
ครอบตัว condition เอาไว้ (อารมณ์คล้ายๆ python และ swift)
แต่ block code จะต้องใส่ {}
ด้วยนะ ละไม่ได้
if-else
if x > 10 {
// x more than 10
} else {
// else...
}
loop
ลูปในโกมีข้อแตกต่างจากภาษา C คือมีแต่คำสั่ง for
ให้ใช้สำหรับลูปทุกรูปแบบ
// รูปแบบ for ธรรมดา
for i := 0; i < 10; i++ {
fmt.Println(i)
}
// รูปแบบ while ก็ยังใช้คีย์เวิร์ด for
i := 0
for i < 10 {
fmt.Println(i)
i++
}
// หรือไม่กำหนดอะไรเลยก็ยังได้ มีค่าเท่ากับ while true
for {
fmt.Println("Aways~")
}
switch
ส่วน switch นั้นไม่ต้องเติม break
ที่ท้ายบล็อกแล้วนะ
switch year {
case 1997: fmt.Println("Philosopher's Stone")
case 1998: fmt.Println("Chamber of Secrets")
case 1999: fmt.Println("Prisoner of Azkaban")
case 2000: fmt.Println("Goblet of Fire")
case 2003: fmt.Println("Order of the Phoenix")
case 2005: fmt.Println("Half-Blood Prince")
case 2007: fmt.Println("Deathly Hallows")
default: fmt.Println("Not Exist!")
}
Array และ Slice
สำหรับตัวแปรแบบ vector ในภาษาโกนั้นมีให้ใช้ 2 แบบคือ
- array: fixed length ขนาดตายตัว ยืดหดไม่ได้
- slice: dynamic length ขนาดเปลี่ยนแปลงได้
array จะใช้สัญลักษณ์ [n]
เพื่อระบุว่าต้องการกี่ช่อง (กำหนดแล้วเปลี่ยนไม่ได้)
var names [3]string
names[0] = "Harry"
names[1] = "Ron"
names[2] = "Hermione"
//หรือถ้าอยากกำหนดค่าไปพร้อมๆ กันเลย
names := [3]string{"Harry", "Ron", "Hermione"}
len(names) // 3: ใช้หาขนาดของ array
ส่วน slice นั้นวิธีประกาศเหมือน array ทุกอย่างแต่ไม่ต้องกำหนดขนาดลงไป
ตอนแรกที่สร้าง slice จะมีขนาดเป็น 0
ส่วนถ้าต้องการเพิ่มข้อมูลเข้าไปจะใช้คำสั่ง append
(ซึ่งเป็นคำสั่งแบบ immutable นะ ถ้าต้องการให้ slice ตัวเดิมเปลี่ยนค่าไปด้วย ต้องเอาตัวแปรไปรับอีกที)
var names []string
names = append(names, "Harry")
names = append(names, "Ron")
names = append(names, "Hermione")
//หรือสำหรับสายย่อ
names := []string{"Harry", "Ron", "Hermione"}
การวนลูปทั้ง array และ slice สามารถใช้ for
แบบธรรมดา หรือใช้คู่กับคำสั่ง range
เพื่อวนแบบ foreach ก็ได้
names := []string{"Harry", "Ron", "Hermione"}
for index := 0, index < len(names); index++ {
fmt.Println(index, names[index])
}
for index, name := range names {
fmt.Println(index, name)
}
Function
ฟังก์ชันของโกเหมือนภาษาตระกูลมีไทป์ทั่วไป โดยเราต้องกำหนดไทป์ของ parameter
func main() {
sayHi("Draco", "Malfoy")
}
func sayHi(firstName string, lastName string) {
fmt.Printf("I'm %s, %s %s.", lastName, firstName, lastName)
}
รวมถึงถ้าฟังก์ชันนั้นมีการรีเทิร์นค่ากลับได้ ก็ต้องกำหนดไทป์ที่ด้านหลังของฟังก์ชันเช่นกัน (ถ้าไม่กำหนดจะถือว่าเป็น void คือฟังก์ชันที่ไม่มีการรีเทิร์นค่า)
func add(x int, y int) int {
return x + y
}
แต่จุดเด่นอีกอย่างของฟังก์ชันในโกคือเราสามารถรีเทิร์นค่ากลับได้หลายค่าในรูปแบบของ tuple ด้วย
func f() (int, string, bool) {
return 123, "val", true
}
func main() {
number, word, boolean := f()
}
และเนื่องจากโกนั้นไม่มีแนวคิดเรื่อง try-catch (สร้าง Exception เพื่อ throw ไม่ได้) ดงนั้นฟังก์ชันไหนที่มีโอกาสเกิด Error ได้ จะนิยมตอบ Error กลับมาด้วยและเป็นเหมือนธรรมเนียมว่าฝั่ง caller จะต้องมีการเช็ก Error ด้วยทุกครั้ง
package main
import (
"errors"
"fmt"
)
func divide(x float64, y float64) (float64, error) {
if y == 0. {
return 0., errors.New("division by zero")
}
return x / y, nil
}
func main() {
r, err := divide(10, 5)
if err != nil {
// TODO: handle error
panic(err)
}
fmt.Println(r)
}
หากเจอ error แล้วอยากให้โปรแกรมหยุดทำงาน สามารใช้คำสั่ง panic()
หรือ os.Exit(1)
ก็ได้
Package
ในการเขียนโปรแกรมจริงๆ เราจำเป็นต้องจัดโค้ดให้เป็นส่วนๆ เพื่อความง่ายต่อการค้นหาและตรวจสอบ ยิ่งถ้าทำงานกันเป็นทีมแล้วไม่มีข้อตกลงอะไรกันเลย โค้ดเละแน่นอน
Package เป็นหนึ่งในฟีเจอร์ที่ช่วยให้เราจัดโค้ดให้เป็นสัดเป็นส่วนได้ โดยเราสามารถแยกไฟล์ของเราออกเป็นส่วนๆ แล้วจะใช้งานส่วนไหนก็ทำการ import
เข้ามาใช้งานได้
สมมุติว่าโปรเจคของเราหน้าตาเป็นแบบนี้
src
└── myProject
├── main.go
└── calcucator
└── main.go
ในโปรเจคนี้เรามี 2 ไฟล์ คือ main.go
และ calcucator/main.go
// calcucator/main.go
func add(x int, y int) int {
return x + y
}
func Calculate(x int, y int) int {
return add(x, y)
}
ทีนี้ถ้าเราอยากเรียกใช้ฟังก์ชันที่อยู่ใน calculate เราก็ต้องโหลดมันเข้ามาซะก่อน
// main.go
import "myProject/calculator"
func main() {
x := calculator.Calculate(1, 2)
// คำถาม
// จะเกิดอะไรขึ้นถ้าเราเรียกใช้คำสั่ง
y := calculator.add(1, 2)
}
ถ้าใครลองโค้ดตัวนี้แล้วจะพบว่าเราสามารถเรียกใช้คำสั่ง calculator.Calculate()
ได้โดยไม่มีปัญหาอะไร แต่พอเป็น calculator.add()
แล้วพังเลย!!
มันต่างกันตรงไหน?
สำหรับคนที่สังเกตเห็นว่าการตั้งชื่อฟังก์ชันมันแปลกๆ ใช่แล้ว! สิ่งที่ต่างกันคือชื่อฟังก์ชันนั่นเอง เพราะว่าโกมีการกำหนดไว้ว่า
Capitalize หรือฟังก์ชันที่ตั้งชื่อขึ้นด้วยตัวอักษาตัวใหญ่ จะถือว่าเป็น public ให้ไฟล์โกอื่นๆ เรียกไปใช้งานได้ แต่ถ้าตั้งชื่อขึ้นด้วยอักษรตัวเล็กจะถือว่าเป็น private ใช้ได้แค่ในไฟล์นั้นเท่านั้น
เรื่องนี้จะต่างจากภาษาอื่นที่นิยมใช้ keyword เช่น public
, private
มาระบุการเข้าถึงฟังก์ชันมากๆ ใช้แรกๆ อาจจะไม่ชินโดยเฉพาะคนที่มาจากภาษาแนว OOP ที่ชอบกำหนดว่าชื่อคลาสเท่านั้นที่จเป็น Capitalize ได้
Struct
สตรัคเป็นการสร้างชนิดข้อมูลเก็บกลุ่มของตัวแปรหลายๆ ชนิดเข้าไว้ด้วยกัน สำหรับภาษาปกติ ถ้าเราต้องการ define กลุ่มของข้อมูลขึ้นมา เรามักจะใช้ class ในการสร้าง แต่เนื่องจากภาษา Go ไม่มี classเราจึงต้องมาใช้วิธีแบบ old school นั่นคือ struct แทน
ภาษา Go มองว่าการเขียนโปรแกรมแบบ OOP โดยเฉพาะในส่วนของ Inheritance (การสืบทอด) นั้นเป็นสิ่งที่โปรแกรมเมอร์มักใช้งานผิด หรือทำให้เกิดการใช้งานผิดได้ง่ายมากเพราะ requirement มาเปลี่ยนหลังจากเริ่มเขียนโค้ดไปแล้ว
โกต้องการให้เราเขียนโค้ดแบบเรียบง่าย ตรงไปตรงมากที่สุด มันเลยตัดคุณสมบัติข้อนี้ออกไป
รูปแบบการสร้าง struct จะใช้คีย์เวิร์ด type
และ struct
แบบนี้
type student struct {
name string
year int
house string
}
หลังจากสร้าง struct ขึ้นมาแล้ว เราก็เอาไปสร้างข้อมูลตามที่กำหนดไว้ได้
hermione := student{
name: "Hermione Granger",
year: 5,
house: "Gryffindor",
}
luna := student{
name: "Luna Lovegood",
year: 4,
house: "Ravenclaw",
}
และเราสามารถใช้ .
เพื่อเข้าถึง property ของ struct ได้
package main
import "fmt"
type student struct {
name string
year int
house string
}
func main() {
harry := student{
name: "Harry Potter",
year: 2,
house: "Gryffindor",
}
fmt.Printf("%v must not go back to Hogwarts", harry.name)
}
และเนื่องจากตามคอนเซ็ปของ struct แล้วมันไม่ใช่ class มันเลยมีได้แค่ properties แต่ไม่มี method
แต่ยังก็ยังดีที่เราสามารถสร้างฟังก์ชันที่ผูกกับ struct ได้ แบบนี้
type student struct {
name string
year int
house string
}
func (s student) spell() {
if s.name == "Harry Potter" {
fmt.Println("Expelliarmus!")
} else {
fmt.Println("Stupefy!")
}
}
func main() {
harry := student{
name: "Harry Potter",
year: 2,
house: "Gryffindor",
}
harry.spell()
}
จากตัวอย่าง เราสร้างฟังก์ชัน spell
แต่มีการกำหนดว่าฟังก์ชันนี้จะใช้ได้กับ struct student
เท่านั้น (มีการกำหนดชื่อตัวแปร s
แทน student เอาไว้ใช้ในส่วนของฟังก์ชันด้วย)
สำหรับเรื่องของ struct เดี๋ยวจะมีเขียนแยกเป็นอีกบทหนึ่งเลย เพราะมีรายละเอียดค่อนข้างเยอะ
ก็จบไปแล้วก็พื้นฐานและภาพรวมของภาษา Go น่าจะเห็นภาพกันแล้ว
คิดว่าสำหรับโปรแกรมเมอร์ที่เคยเขียนภาษาอื่นมาแล้ว เรื่อง syntax ของโกนั้นไม่น่าจะเป็นปัญหา เพราะตัวภาษาก็ออกแบบตามภาษา C อยู่แล้ว แต่ก็ยังมีบางอย่างที่เป็นซิกเนเจอร์ของภาษาโกซึ่งเรายังไม่ได้พูดถึงในบทนี้อยู่นะ เช่น goroutine และ channel
สรุปแล้วก็คือ โกคล้ายๆ ภาษา C ยุคใหม่ที่เขียนง่ายขึ้นหน่อย คอมไพล์เร็วขึ้นเยอะ แต่ด้วยหลักการทำให้ภาษา simple ทุกอย่าง ในให้คนที่เคยเขียนภาษายุคโมเดิร์นอาจจะขัดใจเล็กๆ น้อยๆ ไม่ว่าจะเป็นเรื่องที่โกเป็นภาษาแนว imperative จ๋ามากๆ (แนวภาษา C ไม่ค่อยมีฟีเจอร์จากโลก OOP และ Functional เลย) แถมจำกัดวิธีการเขียนค่อยข้างเยอะ ตัวแปรถ้าประกาศมาแล้วไม่ได้ใช้งาน ก็จะคอมไพล์ไม่ผ่านอีก
ในบทต่อไปเราจะเริ่มลงลึกในรายละเอียดของภาษากัน...
ชอบภาษานี้มั้ย?
[ความเห็นส่วนตัว] ขอตอบว่าถ้าในแง่การทำ system programming ให้มีประสิทธิภาพคือน่าสนใจมากๆ แต่ด้าน syntax ภาษานี้ไม่ผ่าน (เทียบกับทั้งภาษา high-level เช่น JavaScript หรือภาษา low-level กว่าเช่น Rust)
Top comments (2)
ที่แปลก และไม่ชอบ คือ mutability, default value และ error handling
ที่ชอบ คือ compilation speed กับ performance
เรื่อง mutability เห็นด้วยครับ, สำหรับเราที่ไม่ชอบมากที่สุดตอนนี้คือเรื่อง generic type