Published on

Golang Part 1: Go Fundamentals

Authors

Golang Part 1: Go Fundamentals

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

มาปีนี้ต้องมีโปรเจคที่ต้องรันบน Cloud ด้วยความที่ Go นั้นใช้ resource น้อย จึงมีความคิดที่จะเอา Go มาสร้าง API Service แทน

จริงเกิดเป็นบทความชุดนี้ โดยเป้าหมายคือการศึกษาภาษา Go เพื่อนำไปสร้าง API Service โดยจะแบ่งเป็น 3 ตอน

  1. Go Fundamentals: จะเริ่มจากการศึกษาพื้นฐานภาษา Go ก่อนว่าเขียนยังไง
  2. Advanced concepts in Go: Advanced concepts ในภาษา Go ที่คิดว่าน่าจะต้องใช้บ่อยๆ
  3. Go Testing: การเขียน Test ในภาษา Go

มาเริ่มกันเลย

บทความแรกของการศึกษาภาษา Go จะเริ่มจากการศึกษาพื้นฐานภาษา Go ก่อนว่าเขียนยังไง

โดยจุดเริ่มต้นของโปรแกรมในภาษา Go จะเริ่มที่ฟังก์ชัน main และต้องอยู่ใน package main เท่านั้น โดยไฟล์นี้จะอยู่ที่ไหนก็ได้ ชื่ออะไรก็ได้ แต่ขอใช้ชื่อ main.go ละกัน

// main.go
package main

import "fmt"

func main() {
  fmt.Println("Hello world!")
}

วิธีรันโปรแกรม

  • ใช้คำสั่ง go run main.go

  • หรือจะ build เป็น binary file แล้ว ค่อยรันก็ได้

    go build main.go
    
    ./main
    
    # On Windows, run:
    # main.exe
    

Go Module

เรื่องถัดมาที่ต้องทำเมื่อเริ่มสร้างโปรเจค คือ Go Module ซึ่งจะแนะนำให้ตั้งชื่อเป็นชื่อโปรเจคของเรา หรือถ้าต้องการแชร์ให้คนอื่นควรตั้งเป็นชื่อ git repo

go mod init gobasic

Go Packages

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

  1. ให้สร้าง folder ชื่อเดียวกันกับ package เช่นต้องการ package greet
mkdir greet
  1. แนะนำ ให้สร้างไฟล์แรกของ package โดยใช้ชื่อเดียวกันกับ package ด้วย เพื่อให้รู้ว่านี่คือจุดเริ่มต้นของ package นั้นๆ
cd greet
touch greet.go
  1. ถ้าต้องการ expose ตัวแปร หรือ function ให้ package อื่น ใช้งานได้ ให้ตั้งชื่อขึ้นต้นด้วยตัวพิมพ์ใหญ่
greet/greet.go
package greet

func Hi() {
  fmt.Println("Hi there 👋")
}
  1. การเรียกใช้
main.go
// ให้ import โดยใช้ชื่อที่ระบุที่ข้อ 1/ชื่อ package
import "gobasic/greet"

func main() {
  // การเรียกใช้งาน
  greet.Hi()
}

Go Syntax

Variable

ตัวแปรในภาษา Go สามารถประกาศได้ 2 ระดับ คือ

  • package scope คือ ถ้าอยู่ใน package เดียวกันจะสามารถมองเห็นตัวแปรนี้ทั้งหมด (ประกาศนอก function)
  • function scope คือ ตัวแปรที่สามารถใช้ได้ภายใน function นั้นๆ เท่านั้น (ประกาศใน function)

วิธีการสร้างตัวแปรในภาษา Go สามารถสร้างได้หลายแบบ

  • แบบระบุ Type จะใช้ keyword var ตัวตามด้วยชื่อตัวแปร และชนิดของตัวแปร

    // ถ้าไม่ระบุค่าจะได้ค่าเป็น Zero value
    var i int     // 0
    var s string  // ""
    var ok bool   // false
    
    // กำหนดค่าโดยใช้ =
    var i int = 20
    var s string = "hello"
    var ok bool = true
    
  • แบบไม่ระบุ Type หรือ Type inference จะใช้ได้เมื่อมีการกำหนดค่าให้ตัวแปรเท่านั้น จึงจะสามารถละ Type ได้

    // ถ้ากำหนดค่าแล้วละ type ได้
    var i = 20
    var s = "hello"
    var ok = true
    
    // สามารถเขียนแบบ short hand ได้ โดยเอา var ออก แล้วใช้ := แทน =
    i := 20
    s := "hello"
    ok := true
    

Constant

การประกาศค่าคงที่เปลี่ยนจาก var เป็น const ก็จะได้ค่าคงที่แล้ว

const i int = 20
const s string = "hello"
const ok bool = true

สามารถนำมาสร้างเป็น enum ได้โดยใช้ร่วมกับ iota ดังนี้

const (
	sunday = iota  // เริ่มต้นที่ 0 ตัวถัดๆ ไปจะ +1 ไปเรื่อยๆ
  monday
  tuesday
  wednesday
  thursday
  friday
  saturday
)

Function

วิธีการสร้างฟังก์ชันใช้ keywod ว่า func ตามด้วยชื่อฟังก์ชัน ดังนี้

func greeting(){
	fmt.Println("Hello")
}

ระบุพารามิเตอร์โดยใส่ ชื่อตัวแปร ตามด้วยชนิดของตัวแปรในวงเล็บ

func greeting(name string){
	fmt.Println("Hello", name)
}

ถ้าจะคืนค่ากลับไป ให้ระบุชนิดของค่าที่จะคืนกลับไปหลังวงเล็บ

func sum(a, b int) int{
	return a + b
}

ในภาษา Go สามารถคืนค่าได้มากกว่า 1 ค่า ดังนี้

	func swap(a, b int) (int, int){
	return b, a
}

สามารถกำหนดชื่อให้กับค่าที่จะ return ได้

func swap(a, b int) (x int, y int){
  x := b
  y := a
	return x, y
}

Condition

การ control flow ในภาษา Go จะใช้อยู่ 2 อย่าง คือ

  1. IF-ELSE เหมือนกับภาษาอื่น เพียงแค่ไม่ต้องใส่ ()
os := runtime.GOOS
if os == "darwin" {
  fmt.Println("macOS")
} else if os == "linux" {
  fmt.Println("Linux")
} else {
  fmt.Printf("%s.\n", os)
}
  1. Switch ต่างจากภาษาอื่นตรงที่ไม่ต้องใส่ () และไม่ต้องใส่ break
os := runtime.GOOS

switch os {
	case "darwin":
		fmt.Println("macOS")
	case "linux":
		fmt.Println("Linux")
	default:
		// freebsd, openbsd,
		// plan9, windows...
		fmt.Printf("%s.\n", os)
}

// ใช้ร่วมกับ short statment ได้
switch os := runtime.GOOS; os {
	case "darwin":
		fmt.Println("OS X.")
	case "linux":
		fmt.Println("Linux.")
	default:
		// freebsd, openbsd,
		// plan9, windows...
		fmt.Printf("%s.\n", os)
}

Loop

ในภาษา Go จะใช้แค่ for ในการจัดการลูปเท่านั้น แต่เขียนได้หลายรูปแบบ

  • แบบ basic มี 3 ส่วนเหมือนภาษาอื่น ต่างตรงที่ไม่ต้องใส่ ()

    main.go
    package main
    
    import "fmt"
    
    func main() {
      sum := 0
      for i := 0; i < 10; i++ {
        sum += i
      }
      fmt.Println(sum)
    }
    
  • แบบที่ใส่แต่เงื่อนไขอย่างเดียว ซึ่งเอามาประยุกต์สร้าง while loop ได้ ดังนี้

    package main
    
    import "fmt"
    
    func main() {
      sum := 0
      i := 0 // ประกาศตัวแปรข้างนอก for
      for i < 10 { // ใส่แค่เงื่อนไข
        sum += i
        i++  // เพิ่มค่าตอนจบ
      }
      fmt.Println(sum)
    }
    
  • แบบ forever loop คือ ไม่ต้องใส่เงื่อนไข

    package main
    
    import "fmt"
    
    func main() {
      for {
      }
    }
    
  • for range จะเหมือน for each ของภาษาอื่น

    // รูปแบบการประกาศ
    for index, value := range xxx{
    
    }
    
    // ถ้าไม่ต้องการ index ให้ใส่ _ แทน
    for _, value := range xxx{
    
    }
    
    // แต่ถ้าต้องการเฉพาะค่า ก็ไม่ต้องใส่ value
    for index := range xxx{
    
    }
    
     // เมื่อ xxx จะเป็น array/slice/map/channel
    

Go Types

Basic Types

Basic Types ในภาษา Go สามารถแบ่งตามประเภทหลักๆ ได้เป็น 4 ประเภท คือ

  1. boolean มีค่า zero value คือ false
bool
  1. ตัวเลข มีค่า zero value คือ 0
// int เฉยๆ จะได้ขนาดใหญ่สุดตาม cpu
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64

byte  // alias for unit8 (0 - 255)

rune  // alias for int32
      // represents a Unicode code point มีขนาดตั้งแต่ 1 - 4 byte

float32 float64
  1. ตัวเลขจำนวนเชิงซ้อน มีค่า zero value คือ 0
complex64 complex128
  1. string มีค่า zero value คือ ""
string

string

ในการเขียนโปรแกรมเราค่อนข้างที่จะต้องใช้งาน string อยู่บ่อยๆ ดังนั้นจะมาลงลึกเกี่ยวกับ string กันสักหน่อย

จริงๆ แล้ว string ในภาษา Go มันคือ []byte

str := "Hello World"
firstLetter := str[0]

แต่เนื่องจากค่าแต่ละตำแหน่งคือ byte ซึ่งก็คือ uint8 เมื่อสั่ง print ออกมาจะแสดงเป็นตัวเลขของ ASCII

fmt.Println(firstLetter)
// 72

ถ้าต้องการให้แสดงเป็นตัว "H" ต้องแปลงให้เป็น string ก่อน

fmt.Println(string(firstLetter))
// H

และ []byte นี้จะสามารถอ่านค่าได้อย่างเดียว

str[0] = "A" // error

การนับความยาวตัวอักษรในภาษาอังกฤษนั้น ใช้ len() เช่น len(str) แต่เมื่อเอา len() มาใช้กับภาษาอื่นๆ เช่น ภาษาไทย เช่น len("ก") จะนับได้ 3 นั่นก็เพราะว่าภาษาไทย 1 ตัวอักษรนั้นมีการเก็บค่ามากกว่า 1 byte นั่นเอง วิธีการที่ถูกต้อง เราจะต้องแปลงให้เป็น rune ก่อน และการนับจำนวนตัวอักษรให้ใช้ utf8.RuneCountInString("ก") แทน

main.go
package main

import (
	"fmt"
	"unicode/utf8"
)

func main() {
	str := "Hello Gopher"
	fmt.Println(len(str))

	str = "สวัสดีชาวโก"
	fmt.Println(len(str))

	fmt.Println(utf8.RuneCountInString(str))
}

// Output
12
33
11

ถ้าต้องการวนลูปให้ใช้ for range เพราะค่า value จะเป็น rune ออกมาเลย

package main

import (
	"fmt"
)

func main() {
	for _, c := range str {
		fmt.Println(c, string(c))
	}
}

// Output
36263623363336263604363735943634362336503585

ฟังก์ชันของ strings ที่ใช้งานบ่อย

package main

import (
	"fmt"
	"strings"
)

func main() {
	// ตรวจสอบว่ามีคำนี้ในข้อความหรือไม่ case sensitive
	result1 := strings.Contains("Hello Gopher", "go")
	fmt.Println(result1) // false

	// นับคำที่ต้องการหาว่ามีกี่คำในข้อความ
	result2 := strings.Count("สวัสดีชาวโก", "ดี")
	fmt.Println(result2) // 1

	// ตรวจสอบคำขึ้นต้น
	result3 := strings.HasPrefix("สวัสดีชาวโก", "สวั")
	fmt.Println(result3) // true

	// ตรวจสอบคำลงท้าย
	result4 := strings.HasSuffix("สวัสดีชาวโก", "โก")
	fmt.Println(result4) // true

	// ต่อข้อความจาก []string
	result5 := strings.Join([]string{"สวัสดี", "ชาวโก"}, "_")
	fmt.Println(result5) // สวัสดี_ชาวโก

	// แปลงข้อความเป็นตัวพิมพ์ใหญ่ทั้งหมด
	result6 := strings.ToUpper("Hello Gopher")
	fmt.Println(result6) // HELLO GOPHER

	// แปลงข้อความเป็นตัวพิมพ์เล็กทั้งหมด
	result7 := strings.ToLower("Hello Gopher")
	fmt.Println(result7) // hello gopher
}

การแปลงเป็นตัวเลข จะใช้ package strconv มาช่วย

package main

import (
	"fmt"
	"strconv"
)

func main() {
	// แปลงเป็น float ต้องระบุขนาดเสมอ (32/64)
	f, _ := strconv.ParseFloat("3.1415", 64)
	fmt.Println(f) // 3.14

	// แปลงเป็น int ต้องระบุเลขฐานของข้อความที่จะแปลงด้วย
	i, _ := strconv.ParseInt("-42", 10, 64)
	fmt.Println(i) // -42

	// แปลงเป็น int ต้องระบุเลขฐานของข้อความที่จะแปลงด้วย
	u, _ := strconv.ParseUint("42", 10, 64)
	fmt.Println(u) // 42

	// แปลงเป็น bool
	b, _ := strconv.ParseBool("true")
	fmt.Println(b) // true

	// แปลงข้อความเป็นจัวเลข
	i2, _ := strconv.Atoi("-42")
	fmt.Println(i2) // -42

	// แปลงตัวเลขเป็นข้อความ
	s := strconv.Itoa(-42)
	fmt.Println(s) // "-42"
}

Pointer

Pointer เป็นชนิดข้อมูลที่เอาไว้เก็บ memory address ของตัวแปรอื่น มีค่า zero value คือ nil ซึ่งการใช้งาน Pointer ในช่วงแรกๆ อาจทำให้หลายๆ คนงง แต่มีวิธีจำง่ายๆ คือ

  • การประกาศ จะใช้ *T (*หน้าชนิดของข้อมูล)

  • ใช้ operator & หน้าชื่อตัวแปร เมื่อต้องการเอาค่า memory address ออกมา

  • ใช้ operator * หน้าชื่อตัวแปร เพื่อเข้าถึงค่า หรือแก้ไขค่าใน address นั้นๆ

    package main
    
    import (
      "fmt"
    )
    
    func main() {
      name := "Ball"
    
      // สร้าง pointer ของ string
      var s *string
      // ดึงค่า address ออกมา
      s = &name
    
      // เข้าถึงค่าใน address ที่อ้างอิงถึง
      fmt.Println(*s)
    
      // แก้ไขค่าใน address ที่อ้างอิงถึง
      *s = "Somprasong"
      fmt.Println(name)
    }
    

การส่งค่าผ่านฟังก์ชันใน Go นั้น จะเป็นการส่งแบบ Pass by Value เสมอ ดังนั้นเมื่อส่งค่าผ่านฟังก์ชันมันจะทำสำเนาตัวแปรใหม่ออกมา ทำให้ตัวแปรที่ส่งไป และตัวแปรในฟังก์ชันคือคนละตัวกัน ตัวอย่าง

package main

import (
  "fmt"
)

func inc(n int) int {
  return n +1
}

func main() {
  i := 1

  result := inc(i)

  fmt.Println(result)
}

// Output
// 2

จากตัวอย่างข้างบนตัวแปร i และ n จะเป็นตัวแปรคนละตัวกัน ถ้าต้องการให้ฟังก์ชัน inc() ทำการแก้ไขค่า i เลย ให้เปลี่ยนเป็นรับค่าเป็น pointer แทน มันจะทำสำเนาตัวแปรใหม่ แต่เก็บค่า memory address ของ i เอาไว้ ดังนั้น เมื่อเราแก้ไขค่าใน memory address นั้น ค่าของ i ก็จะเปลี่ยนไปด้วย ตัวอย่าง

package main

import (
  "fmt"
)

func inc(n *int){
  *n = *n + 1
}

func main() {
  i := 1

  inc(&i)

  fmt.Println(i)
}

// Output
// 2

Array

Array ในภาษา Go เมื่อประกาศขึ้นมาแล้วจะไม่สามารถเพิ่ม หรือลดขนาดได้แล้ว หรือเรียกว่า Immutable

  • วิธีการการประกาศ Array ใช้ [ระบุจำนวน]T{}

    // จะได้ int มา 4 ตัว ทุกตัวมีค่าเป็น zero value คือ ""
    var x [4]int = [4]int{}
    
    // หรือ
    var x = [4]int{}
    
    // หรือ
    x := [4]int{}
    
    // หรือ จะใส่ค่าไปตั้งแต่ประกาศเลยก็ได้ โดยตำแหน่งที่ไม่ใส่ค่ามาจะมีค่าเป็น zero value คือ 0
    x := [4]int{1, 2, 3}
    
  • การเข้าถึงค่าใน Array

    fmt.Println(x[0])  // 1
    fmt.Println(x[1])  // 2
    fmt.Println(x[2])  // 3
    fmt.Println(x[3])  // 0
    
  • การแก้ไขค่าใน Array

    x[1] = 10
    
    fmt.Println(x[1])  // 10
    
  • การใช้งานกับ for range

    // ถ้าต้องการเฉพาะ index
    func rangeIndexOfArray() {
      a := [...]int{1, 2, 3, 4, 5}
    
      for i := range a {
        fmt.Println(a[i])
      }
    }
    
    // ถ้าต้องการเฉพาะ value ด้วย
    func rangeIndexOfArray() {
      a := [...]int{1, 2, 3, 4, 5}
    
      for i, v := range a {
        fmt.Println(i, v)
      }
    }
    

Slice

ข้อจำกัดหนึ่งของ Array คือ จำเป็นต้องรู้ขนาดก่อน แต่เวลาใช้งานจริงเราไม่รู้ขนาดว่าจะต้องประกาศ Array ขนาดเท่าไหร่ ในภาษา Go จึงมี Slice ซึ่งเป็น mutable ที่มีขนาดไม่จำกัด เพิ่ม-ลดได้ มาให้ใช้งาน มีค่า zero value คือ nil

  • วิธีการประกาศ Slice ใช้ []T{} เหมือน Array แต่ไม่ต้องระบุขนาด

    // จะมีค่าเป็น zero value คือ nil ยังใช้งานไม่ได้
    var x []int
    
    // ต้อง allocate memory ให้ก่อนถึงจะใช้งานได้
    x = make([]int, 4) // จะต้องระบุขนาดเริ่มต้นให้ก่อน ทุกตำแหน่งจะได้ค่า zero value ของ type นั้นๆ
    // []int{0, 0, 0, 0}
    
    // หรือสร้างแบบว่างๆ
    x := []int{}
    
    // หรือ จะใส่ค่าไปตั้งแต่ประกาศเลยก็ได้
    x := []int{1, 2, 3}
    
  • การเพิ่มข้อมูลใน Slice ใช้ฟังก์ชัน append()

    x := []int{1, 2, 3}
    
    x = append(x, 4)
    x = append(x, 5)
    
    fmt.Printf("%#v\n", x)
    
    // []int{1, 2, 3, 4, 5}
    
  • ถ้าต้องการดูขนาดใน Slice และ Array ใช้ฟังก์ชัน len() และจะมี cap() เอาไว้ให้ดูว่ามีพื้นที่สำหรับเก็บข้อมูลกี่ตัวด้วย

    y := len(x)
    z := cap(x)
    
    fmt.Printf("%#v, len=%v\n, cap=%v", x, y, z)
    
    // []int{1, 2, 3, 4, 5}, len=5, cap=6
    
  • การ slice ข้อมูลใน Slice และ Array ใช้ [index_เริ่มต้น:index_ที่ต้องการ+1]

    //          0   1   2   3   4   5   6   7   8
    x := []int{10, 20, 30, 40, 50, 60, 70, 80, 90}
    
    // เอาตั้งแต่ 0 จนถึงตัวสุดท้าย
    y := x[0:]
    
    // หรือเขียนแบบนี้ก็ได้
    y := x[:]
    
    // ถ้าต้องการ 30, 40, 50
    y :=x[2:5] // ค่า 30 คือ index ที่่ 2 ส่วน 50 คือ index ที่ 4 ดังนั้นต้อง + 1 = 5
    
    // ถ้าต้องการต้องแต่เริ่มต้น จนถึงตำแหน่งที่ต้องการ สามารถละ index เริ่มต้นได้
    y :=x[:5]
    
  • การลบข้อมูลออกจาก Slice จะใช้วิธี slice ข้อมูลออกแบบ 2 ก้อน แล้วนำมาต่อกันใหม่ เช่น

    words := []string{"A", "B", "C", "D", "E"}
    
    // ถ้าต้องการจะลบ "C" ออกไป ซึ่งก็คือตำแหน่งที่ 2 จะต้องได้ {"A", "B"} + {"D", "E"}
    
    // จะต้องได้แบบนี้ แต่ "D", "E" จะต้องไม่ใช่ใส่เองแบบนี้
    // words = append(words[:2], "D", "E")
    
    // ถ้า slice words[3:] ลงไปตรงๆ ก็ไม่ได้
    // words = append(words[:2], words[3:])
    
    // จะต้องใช้ spread operator แทนแบบนี้
    
    words = append(words[:2], words[3:]...)
    
    fmt.Println(words)
    
  • การใช้งานกับ for range

    // ถ้าต้องการเฉพาะ index
    func rangeIndexOfSlice() {
      a := []int{1, 2, 3, 4, 5}
    
      for i := range a {
        fmt.Println(a[i])
      }
    }
    
    // ถ้าต้องการเฉพาะ value ด้วย
    func rangeIndexOfSlice() {
      a := []int{1, 2, 3, 4, 5}
    
      for i, v := range a {
        fmt.Println(i, v)
      }
    }
    

Map

Map เป็นการจัดเก็บข้อมูลแบบ key-value และมีค่า zero value คือ nil

  • วิธีการประกาศ

    // ถ้าประกาศขึ้นมาลอยๆ จะมีค่าเป็น zero value คือ nil
    var m map[string]string
    
    m := make(map[string]string)
    // หรือ
    m := map[string]string{}
    
    // หรือ จะใส่ค่าไปตั้งแต่ประกาศเลยก็ได้
    m := map[string]string{
      "a": "apple",
      "b": "banana", // ต้องปิดด้วย , เสมอ
    }
    
  • การอ่านค่า

    fmt.Printf("%v\n", m["a"])
    
    // apple
    

    กรณีที่อ่านค่าโดยใช้ key ที่ไม่มีอยู่จริงจะได้ zero value กลับมา ทำให้เกิดปัญหาว่า key นั้นมีจริง และมีค่าตามนั้น หรือ key นั่นไม่มี ดังนั้นจะต้องตรวจสอบก่อน

    fmt.Printf("%v\n", m["c"])
    // ""
    
    v, ok := m["c"]
    if ok {
      fmt.Printf("%v\n", m["c"])
    }
    
  • การแก้ไขข้อมูล

    m["b"] = "berry"
    
    fmt.Printf("%#v\n", m)
    
    // map[string]string{"a":"apple", "b":"berry"}
    
  • การเพิ่มข้อมูล

    m["c"] = "cranberry"
    
    fmt.Printf("%#v\n")
    
    // map[string]string{"a":"apple", "b":"banana", "c":"cranberry"}
    
  • การลบข้อมูล

    delete(m, "b")
    
    fmt.Printf("%#v\n", m)
    
    // map[string]string{"a":"apple", "c":"cranberry"}
    
  • การใช้งานกับ for range

    // จะได้ key แทน index
    func rangeOfMap() {
      m := map[string]string{
        "a": "apple",
        "b": "banana",
      }
    
      for k, v := range m {
        fmt.Println(k, v)
      }
    }
    

Struct

ในภาษา Go สามารถสร้างชนิดข้อมูลใหม่ได้โดยการใช้ struct

  • วิธีการสร้างชนิดข้อมูลใหม่

    type Rectangle struct {
      Width  float64
      Height float64
    }
    
  • การ init

    rec := Rectangle{} // จะมีค่าเป็น empty -> {}
    
    // หรือจะกำหนดค่าไปเลยก็ได้
    
    rec := Rectangle{
      Width: 10,
      Height: 20,
    }
    
  • การกำหนดค่า

    rec := Rectangle{}
    
    rec.Width = 10
    rec.Height = 20
    
  • การอ่านค่า

    w := rec.Width
    
  • Method จากตัวอย่างด้านบน ถ้าต้องการคำนวนหาพื้นที่ของ struct สามารถสร้าง function มาจัดการได้ แต่ถ้าอยากได้ method style ต้องเขียนแบบ recevier function

    package main
    
    import "fmt"
    
    type Rectangle struct {
      Width  float64
      Height float64
    }
    
    // fucntion
    func Area(rec Rectangle) float64 {
    	return rec.Width * rec.Height
    }
    
    // recevier function
    func (rec Rectangle) Area() float64 {
    	return rec.Width * rec.Height
    }
    
    func main() {
      rec := Rectangle{
        Width:  10,
        Height: 20,
      }
    
      // fucntion style
      fmt.Println(Area(rec))
    
      // method style
      fmt.Println(rec.Area())
    }
    
  • Method Sets Receiver function โดยปกติจะเป็นแบบ Value Receiver ดังนั้นถ้าต้องการเปลี่ยนแปลงค่าใน struct จะทำไม่ได้เพราะว่า Go จะทำการ copy struct ขึ้นมาใหม่แล้วส่งเข้าไป ทำให้เป็นคนละตัวกัน ดังนั้นในกรณีที่ต้องการเปลี่ยนแปลงค่าใน struct จะต้องใช้ Pointer Receiver แทนให้มัน copy memory address ส่งไปแทน

    package main
    
    import "fmt"
    
    type Rectangle struct {
      Width  float64
      Height float64
    }
    
    func (rec Rectangle) SetHeightValue(h float64) {
      rec.Height = h
    }
    
    func (rec *Rectangle) SetHeightPointer(h float64) {
      rec.Height = h
    }
    
    func main() {
      rec := Rectangle{
        Width:  10,
        Height: 20,
      }
    
      fmt.Println(rec)
    
      rec.SetHeightValue(30)
      fmt.Println(rec)
    
      rec.SetHeightPointer(30)
      fmt.Println(rec)
    }
    
    // Output
    {10 20}
    {10 20}
    {10 30}
    
  • Type Embedding โดยปกติเวลาสร้าง struct เราสามารถเอา struct ตัวอื่นมาเป็นชนิดข้อมูลใน struct อีกตัวได้แบบนี้

    package main
    
    import "fmt"
    
    type Shape struct {
      Name string
    }
    
    type Rectangle struct {
      Width  float64
      Height float64
      Shape  Shape
    }
    
    func main() {
      rec := Rectangle{
        Width:  10,
        Height: 20,
        Shape:  Shape{Name: "Rectangle"},
      }
    
      fmt.Println(rec.Shape.Name)
    }
    
    // Output
    Rectangle
    

    แต่ถ้าชื่อ filed เป็นชื่อเดียวกับ struct เราสามารถละชื่อ field ได้ และโปรแกรมก็ยังทำงานได้เหมือนเดิม

    type Rectangle struct {
      Width  float64
      Height float64
      Shape
    }
    
    func main() {
      rec := Rectangle{
        Width:  10,
        Height: 20,
        Shape:  Shape{Name: "Rectangle"},
      }
    
      fmt.Println(rec.Shape.Name)
    }
    
    // Output
    Rectangle
    

    ซึ่งวิธีการแบบด้านบนนี้เรียกว่า Type Embedding และเมื่อทำแบบนี้จะได้คุณสมบัติการ promoted fields เพิ่มมาด้วย ทำให้ใน Rectangle สามารถเรียกใช้ Name ได้เลย

    func main() {
      rec := Rectangle{
        Width:  10,
        Height: 20,
        Shape:  Shape{Name: "Rectangle"},
      }
    
      fmt.Println(rec.Name)
    }
    
    // Output
    Rectangle
    

Go Interface

Interface ในภาษา Go จะอยู่ 2 แบบ คือ

  1. Empty Interface รูปแบบ คือ interface{} โดยจะรับค่าเป็นอะไรก็ได้ เช่น
var a interface{}

a = 10
a = "ten"
a = true
a = book{}

s := "hello"
a = s
  1. ถ้าไม่ใช่ Empty interface คือ มี method เพื่อเอาไว้กำหนดข้อตกลง ว่าจะต้องมีคุณลักษณะแบบไหนบ้างเท่านั้น
type Phone interface {
  Call(number string)
}

การ implement จะใช้วิธีการ implement แบบ Implicitly คือ ขอแค่หน้าตาเหมือนก็ถือว่า implement แล้ว เช่น

type Samsung struct {
  Name string
}

func (s Samsung)Call(number string) {
  // แบบนี้ถือว่า implement Phone แล้ว
}

การจัดการ Error ในภาษา Go

Go Error

เนื่องจากภาษา Go นั้นจะไม่มี try-catch ทำให้ต้องจัดการ error ณ จุดที่เกิด error เลย

ซึ่งวิธีการจัดการ Error จะใช้วิธีตรวจว่ามีค่า error หรือไม่ ซึ่งถ้ามีจะต้องมีค่า!=nil เพราะ Error นั้นเป็น interface ตัวหนึ่ง ทำให้มี zero value คือ nil

n, err := strconv.Atoi("5s")
if err !=nil {
  // อาจจะ log หรือส่งออกไปให้ที่อื่นจัดการต่อ
  return err
}

และเราสามารถสร้าง Error ขึ้นมาได้จาก package errors

func findIndex(s []int, num int) (int, error) {
	for i, v := range s {
		if v == num {
			return i, nil
		}
	}
	return 0, errors.New("number not found")
}

และเนื่องจาก Error เป็น interface ที่หน้าตาแบบนี้

type error interface {
  Error() string
}

ดังนั้น struct อะไรก็ได้ที่มี method Error() string ก็เป็น Erorr ได้เหมือนกัน

Panic & Recover

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

func main() {
  fmt.Println(1)
  fmt.Println(2)
  panic("Fail")
  fmt.Println(3)
  fmt.Println(4)
  fmt.Println(5)
}

// Output
1
2
panic: Fail

ดักจับ Panic ด้วย Recover

เมื่อมีการเรียกฟังก์ชันซ่อนๆ กัน แล้วเกิด panic ขึ้น ระบบจะดูว่าในฟังก์ชันที่เรียกฟังก์ชันทีเกิด panic นั้น ได้มีการจัดการ panic หรือไม่ ถ้าไม่มีก็จะส่ง panic ต่อขึ้นไปเรื่อยๆ จนถึง main() ถ้าไม่การการจัดการก็จะจบการทำงานของโปรแกรม ซึ่งสามารถดักจับ panic ได้ด้วยการใช้ recover() ตัวอย่าง

func a() {
  b()
}

func b() {
  panic("Panic in b")
}

func main() {
  a()
  fmt.Println("Completed")
}

// Output
panic: Panic in b

แบบนี้จะโปรแกรมจะหยุดการทำงานเพราะ panic แต่ถ้าเพิ่ม recover เข้าไปใน a() โปรแกรมก็จะทำงานได้ต่อจนจบ

func a() {
  defer func() {
    if r:= recover(); r != nil {
      fmt.Println(r, "Recover in a")
    }
  }()
  b()
}

func b() {
  panic("Panic in b")
}

func main() {
  a()
  fmt.Println("Completed")
}

// Output
Panic in b Recover in a
Completed

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