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)

39 Total Views 3 Views Today
Ta

Ta

สิ่งมีชีวิตตัวอ้วนๆ กลมๆ เคลื่อนที่ไปไหนโดยการกลิ้ง .. ถนัดการดำรงชีวิตโดยไม่โดนแสงแดด
ปัจจุบันเป็น Senior Software Engineer อยู่ที่ Centrillion Technology
งานอดิเรกคือ เขียนโปรแกรม อ่านหนังสือ เขียนบทความ วาดรูป และ เล่นแบดมินตัน