Install on Linux

rm -rf /usr/local/go && tar -C /usr/local -xzf go1.25.5.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin
go version

Project Setup

ถ้าเริ่มโปรเจคง่าย ๆ ใช้เอง

go mod init hello-world

ถ้าเริ่มโปรเจคแบบมาตรฐาน ทำ Open source ต้องใส่เป็น repository host หรือ domain name

go mod init github.com/nattrio/hello-world

เสร็จแล้วมันจะไปสร้าง Go module คือไฟล์ go.mod เพื่อช่วยจัดการ packages และ dependency ต่าง ๆ โดยสามารถกำหนดเวอร์ชั่นได้ ทำให้การจัดการมีประสิทธิภาพมากขึ้น

หากต้องการโหลด dependency ที่ต้องการในโปรเจค หรือมีการแจ้งเตือนว่าจำเป็นต้องใช้ ให้ใช้คำสั่ง go mod tidy

  • ชื่อไฟล์ของ go จะใช้เป็นตัวเล็กทั้งหมดและเว้นด้วย underscore เช่น cal_distance.go
  • ชื่อฟังก์ชันภายใน go จะใช้เป็น camelCase เช่น sumOfNumber
  • Entry point package คือ main
  • ในแต่ละ package (folder) ควรมีไฟล์ที่ตั้งชื่อเดียวกันกับ package ข้างในด้วย เช่น movie/movie.go

Import

เราสามารถใส่ underscore _ วางไว้หน้า package เพื่อระบุว่าให้ import เข้ามาแม้จะยังไม่ใช้งาน เนื่องจากบาง package จะเริ่ม init() ตอนเรียกใช้

import (
	_ "github.com/proullon/ramsql/driver"
)

Package

เป็นโฟลเดอร์ที่เอาไว้ใส่ source files ใน directory เดียวกัน และ compile ด้วยกัน

  • Field ขึ้นต้นด้วยตัวใหญ่ Public (มองเห็นจากนอก package ได้)
  • Field ขึ้นต้นด้วยตัวเล็ก Private (มองเห็นเฉพาะภายใน package นั้นๆ)
// Public
func Review(name string, rating float64) {
	fmt.Printf("!!! I reviewed %s and it's rating is %f\n", name, rating)
}
 
// Private
func review(name string, rating float64) {
	fmt.Printf("!!! I reviewed %s and it's rating is %f\n", name, rating)
}

Init Function

init() คือ ฟังก์ชันที่จะถูกเรียกใช้งานทันที เมื่อมีการเข้าถึง package ใดๆ

func init() {
	fmt.Println("This will get called on main initialization")
} // [1]
 
func main() {
	fmt.Println("My Wonderful Go Program")
} // [2]
 

Syntax

Overview

package main
 
func main() {
 
}

Variables

// Variables
var ok bool
var s string = "hello"
 
// Type Inference
var ok = true
s := "hello"

โดยการใช้แบบ Type Inference มีข้อจำกัดคือไม่สามารถประกาศนอก body ของ function หรือในระดับ package ได้ ต้องใช้แบบ var เท่านั้น

Multiple declaration

s, ok := "hello", true

Constants

const defaultValue int = 1
const defaultTitle = "Go"
const word = "Hello"
  • iota เป็น keyword ที่ใช้ทำ Enum โดยกำหนดตัวแปรที่ประกาศให้ค่าตั้งต้นเป็น int = 0 ตัวถัดไปก็จะเพิ่มขึ้นทีละ 1
  • ถ้าประกาศตัวแปร iota + 1 ตัวแรกก็จะค่ามีตั้งต้นเป็นเริ่มที่ 1 แล้วก็นับถัดไปเหมือนเดิม
const (
	sunday = iota
	monday
	tuesday
	wednesdat
	thursday
	friday
	saturday
)

ต้องการประกาศให้ constant มี type เป็น day

type day int
const (
	sunday day = iota
	monday
	tuesday
	wednesdat
	thursday
	friday
	saturday
)

Conditions

If Statement

if name != "" && (a > 1 || b < 2) {
	fmt.Println("Hello", name)
} else {
	fmt.Println("Hello friend")
}

If with a Short Statement (Scope เฉพาะใน if เท่านั้น)

limit := 225.0
v := math.Pow(10, 2)
if v < limit {
	fmt.Println("power:", v)
} else {
	fmt.Printf("power: %g over limit %g", v, limit)
}
 
// Short statement
limit := 225.0
if v := math.Pow(10, 2); v < limit {
	fmt.Println("power:", v)
} else {
	fmt.Printf("power: %g over limit %g", v, limit)
}

Switch Case

Value switch

today := "Saturday"
switch today {
case "Saturday":
	fmt.Println("Playing")
	fallthrough
case "Sunday", "Monday":
	fmt.Println("Relax")
default:
	fmt.Println("Working!!!")
}

Condition switch

num := 1
switch {
case num >= 0:
	fmt.Println("Great!!!")
case num < 0:
	fmt.Println("Sad")
default:
	fmt.Println("Confused")
}

fallthrough คือบอกว่าให้ทำ case ด้านล่างถัดไปอีก 1 case ด้วย

For Loop

Golang มีการทำวนซ้ำแบบเดียวคือ for loop ไม่มี keyword while loop การทำ for loop มี 3 รูปแบบ ดังนี้

for i := 0; i < 10; i++ {
	// A loop with 3 components
}
 
for i < 10 {
	// A loop with a condition (เทียบเท่า while)
}
 
for {
	// An infinite loop
}

การใช้ for loop กับสมาชิกใน Array

skills := [3]string{"JS", "Go", "Python"}
 
for i := 0; i < len(skills); i++ {
	fmt.Println(skills[i])
}
 
/* OR */
for i := range skills {
	fmt.Println(skills[i])
}

จริง ๆ แล้ว range จะคืนค่าออกมาได้ 2 ค่า คือ index, value

for i, val := range skills {
	fmt.Println("index:", i, "value:", val)
}

เราสามารถละค่าตัวแปรแรกเพื่อใช้แต่ตัวที่สองได้ โดยใช้ _

for _, val := range skills {
	fmt.Println("value:", val)
}

Type conversions

การเปลี่ยนชนิดข้อมูลให้ตัวแปร

var i int = 1
fmt.Printf("type: %T, value: %v\n", i, i)
 
var f float64 = float64(i) // type conversion
fmt.Printf("type: %T, value: %v\n", f, f)

ในกรณีเปลี่ยนจาก string เป็นข้อมูลชนิดตัวเลข ต้องใช้ Package strconv

v := "42"
s, err := strconv.Atoi(v)
if err != nil {
	fmt.Println(err)
	return
}
fmt.Println(s)
 
// convert int to string
i := 10
n := strconv.Itoa(i)
fmt.Printf("%T, %v\n", n, n)

Error handling

Error มี type เป็น interface จึงมี zero value คือ nil

  • ลองใช้ Error handling กับการหารด้วย 0
func divide(a, b float64) (float64, error) {
	if b == 0 {
		err := fmt.Errorf("can't divide by zero")
		return 0, err
	}
	r := a / b
	return r, nil
}
 
func main() {
	r, err := divide(1, 0)
	if err != nil {
		fmt.Println("handler err:", err)
		return
	}
	fmt.Println(r, err)
}

Defer

เป็น keyword ที่ใช้ดำเนินการบางอย่างก่อนจบการทำงาน

  • เช่น การเปิด-ปิดไฟล์ สำหรับบางภาษาถ้าเปิดแล้วก็ต้องอย่าลืมปิดตอนท้ายด้วย ซึ่ง Defer จะเข้ามาช่วยโดยเมื่อเปิดไฟล์แล้วไม่มี error ก็สั่ง defer ปิดไฟล์ไฟล์ไว้ได้เลย
func main() {
	defer fmt.Println("Bye, World!")
 
	fmt.Println("Hello, World!")
}
 
// Hello, World!
// Bye, World!

defer เป็น stack ซึ่งสามารถสั่งหลายครั้งซ้อนกันได้ โดยทำจากล่างขึ้นบน หรือหยิบของที่ใส่ไว้ล่าสุดมาก่อน

func main() {
	defer fmt.Println("defer#1")
	defer fmt.Println("defer#2")
	defer fmt.Println("defer#3")
 
	fmt.Println("Hello, World!")
}
 
// Hello, World!
// defer#3
// defer#2
// defer#1

การ Defer anonymous function

func main() {
	fmt.Println("counting")
	for i := 0; i < 3; i++ { // defer an anonymous function call
		defer func(n int) {
			fmt.Println(n)
		}(i)
	}
	fmt.Println("done")
 
	// Output:
	// counting
	// done
	// 2
	// 1
	// 0
}

Data Structure

Data Types

ในภาษา go ส่วน type ไม่ได้มีความซับซ้อน เพราะไม่ใช่ OOP แบ่งเป็น 4 กลุ่มหลัก ดังนี้

รายละเอียด:

  • complex64 จะแบ่งเป็นจำนวนเต็ม 32 bit และจำนวนจินตภาพ 32 bit
  • การใช้ int เฉยๆ จะเป็นการหมายถึง int ที่มีขนาดมากที่สุดเท่าที่ CPU บนเครื่องเราใช้ เช่น เช่น CPU 64 bit ก็จะได้ int64
  • uint หมายถึง unsigned int
  • byte ใช้หมายถึงอักขระ ASCII code ขนาด 8 bit
  • rune ใช้เป็น Unicode code point (utf) มีขนาดได้ 1 - 4 byte
  • pointer มี zero value คือ nil (เทียบเคียงเหมือน null ในภาษาอื่น)

Rune

  • A rune is a character. That’s it. Rune is also an alias for int32
  • ถ้าเป็นตัวอักขระ string เช่น abc ปกติจะไม่มีปัญหา เพราะสามารถรองรับใน byte (8 bit) ได้
  • แต่ถ้าเป็นภาษาหรืออักขระอื่น ๆ จะเกินที่รองรับใน 8 bit ทำให้มีปัญหาในการนับจำนวนอักขระที่จะเกินจากความเป็นจริง ตัวอย่างการนับ
  • Formatting: print rune ปกติออกมาเป็นเลข ถ้าอยากให้เป็น char ต้องกำหนด format
var r rune = '😝'	
fmt.Println("r:", r) // r: 128541
fmt.Printf("r: %c\n", r) // r: 😝

ถ้าอยากให้ format ออกมาตรงค่าโดยไม่ต้องจำแยก type ให้ใช้ %#v

Array

  • Immutable (เปลี่ยนแปลงขนาดไม่ได้)
  • ประกาศโดยการวาง [ ] ที่มีตัวเลข วางไว้หน้า type
  • Index เริ่มที่ 0 และเก็บค่า default เป็น zero value ของ type ที่ประกาศ
var fourNum [4]int
fourNum[0] = 1
fourNum[2] = 3
  • การ Assign ค่า Array
// var skills [3]string = [3]string{"JS", "Go", "Python"}
skills := [3]string{"JS", "Go", "Python"}
 
fmt.Println(skills[2])

Array ไม่ค่อย flexible เพราะปรับขนาดไม่ได้ กว่าเราจะรู้ว่าของที่เราต้องการใช้มีขนาดเท่าไหร่ก็มักอยู่ในช่วง runtime แล้ว ทำให้ลำบากคำนวณว่าจะต้องประกาศ Array ขนาดเผื่อเท่าไหร่ถึงเหมาะสม Go จึงออกแบบ Slice มาใช้

Slice

  • Mutable (เปลี่ยนแปลงขนาดได้)
  • ประกาศโดยการวาง [ ] โดยไม่มีตัวเลข วางไว้หน้า type
  • Zero value คือ nil ฉะนั้นจึงนับ Slice เป็น Pointer ประเภทนึง
  • ประกาศด้วย make(type, สมาชิกตั้งต้น) จะทำหน้าที่ allocate memory ให้ โดยสมาชิกจะเป็น zero value ตาม type
  • สมาชิกเเดิมเป็น 0 ก่อนก็ได้ แล้วค่อยเติมของทีหลัง
  • สามารถเติมของด้วย append (slice ตั้งต้น, ค่าที่ต้องการเติมโดย type ต้องเหมือนกับสมาชิก slice) และสามารถเพิ่มทีละหลายค่าได้
var num []int
nums = make([]int, 4) // [0, 0, 0, 0]
nums[0] = 1
nums[2] = 2
nums = append(nums, 20, 30)

ภายใน (Internal) ของ Slice ประกอบไปด้วย 3 ค่า อ้างอิงกับ make()

  1. pointer มีหน้าที่ชี้ไปหา Array จริง ๆ ตัวนึง แปลว่าเบื้องหลัง Slice ทุกตัวจะมี Array อยู่เสมอ
  2. length เก็บจำนวนสมาชิกที่มันชี้ไป len()
  3. capacity ความจุของ array cap()

การหั่น (slice) ด้วย colon แบบ half-open range

skills := []string{"Go", "JS", "Ruby"}
 
fmt.Println(skills[0:2])           // [Go JS]
fmt.Println(skills[:len(skills)])  // All
fmt.Println(skills[0:])            // All
fmt.Println(skills[:])             // All

Arguments to variadic functions เราสามารถส่ง slice เป็น variadic function โดยตรง ซึ่งมีค่าเท่ากับการ unpack สมาชิกแต่ละตัว โดยใช้ ...

xs := []float64{1, 2, 3, 4}
ys := []float64{5, 6, 7}
 
var xys []float64
xys = append(xs, ys...)
// xys = append(xs, ys[0], ys[1], ys[2])

Demo 1

เบื้องหลัง Slice ทุกตัวจะมี Array อยู่ข้างล่างเสมอ (Underlying array)

  • สร้าง skills เป็น slice เก็บ string แล้วหั่นเป็น s1 และ s2
  • ถ้าเปลี่ยนค่าใน s1[1] เป็น “Gopher” จะทำให้ค่าที่ show จะเปลี่ยนไป โดยกระทบทั้ง s1, s2 และ skills ด้วย
  • เนื่องจากว่า s1, s2 อ้างอิงไปที่ array ตัวต้นทางคือ skills
func show(tag string, sk []string) {
	l := len(sk)
	fmt.Printf("%s: len: %d -- %v\n", tag, l, sk)
}
 
func main() {
	skills := []string{"JS", "Go", "Python"}
 
	s1 := skills[0:2]
	show("s1", s1)
 
	s2 := skills[1:3]
	show("s2", s2)
 
	s1[1] = "Gopher" // ถ้าเปลี่ยนค่าใน index 1 เป็น "Gopher"
	show("s1", s1)
	show("s2", s2)
	show("skills", skills)
}
 
/* Before */
// s1: len: 2 -- [JS Go]
// s2: len: 2 -- [Go Python]
 
/* After */
// s1: len: 2 -- [JS Gopher]
// s2: len: 2 -- [Gopher Python]
// skills: len: 3 -- [JS Gopher Python]

Demo 2

  • skills มี capacity 3 ช่อง
  • s1 := skills[0:2] แม้ว่าจะหั่น (slice) คือเอาแค่ 2 ตัวหน้าสุด แต่ช่องว่างด้านหลังตาม capacity ยังอยู่ จึงนับได้เป็น 3
  • s2 := skills[1:3] หั่นเอาแค่ 2 ตัวท้าย โดยทิ้งตัวข้างหน้าไป ทำให้ capacity เหลืออยู่แค่ 2 เท่านั้น
func show(tag string, sk []string) {
	l := len(sk)
	c := cap(sk)
	fmt.Printf("%s: len: %d cap: %d -- %v\n", tag, l, c, sk)
}
 
func main() {
	skills := []string{"JS", "Go", "Python"}
 
	s1 := skills[0:2]
	show("s1", s1)
 
	s2 := skills[1:3]
	show("s2", s2)
}
 
// s1: len: 2 cap: 3 -- [JS Go]
// s2: len: 2 cap: 2 -- [Go Python]
  • ถ้าเราทำการ s2 = append(s2, "C++") Go จะสร้าง array ตัวใหม่สำหรับ s2 โดย copy ค่าเดิมมาด้วย ทำให้ตอนนี้ s1, s2 ไม่ได้ชี้ array ตัวเดียวกันแล้ว
  • ถ้าเปลี่ยนค่าใน s2[0] เป็น “Gopher” จะเห็นว่า s1, s2 ตรงที่เคย overlap นั้นแสดงผลไม่เหมือนกัน เพราะไม่ได้ส่งผลกระทบต่อกันแล้ว รวมถึงไม่ส่งผลต่อ skills
func main() {
	skills := []string{"JS", "Go", "Python"}
 
	s1 := skills[0:2]
	show("s1", s1)
 
	s2 := skills[1:3]
	s2 = append(s2, "C++") // append
	show("s2", s2)
 
	s2[0] = "Gopher" // change
	show("s1", s1)
	show("s2", s2)
}
 
// s1: len: 2 cap: 3 -- [JS Go]
// s2: len: 3 cap: 4 -- [Go Python C++]
 
// s1: len: 2 cap: 3 -- [JS Go]
// s2: len: 3 cap: 4 -- [Gopher Python C++]

Pointer

ทุกตัวแปรจะมีการจองที่จัดเก็บข้อมูลไว้ โดย memory address

  • pointer มี Zero value คือ nil
  • pointer ไม่สามารถใช้ทำ Arithmetic Operation ได้
  • * ใช้ประกาศ pointer โดยการวางไว้หน้า type
  • * ยังสามารถใช้ dereference ตัวแปร pointer เพื่อเข้าถึงค่าที่ชี้ไปได้ เช่น *addr
  • & วางไว้หน้าตัวแปร เพื่อใช้ reference ไปยัง memory address เช่น &price
var price int = 100
var addr *int = &price
 
fmt.Println("[1]", price, &price)
fmt.Println("[2]", *addr, addr)
 
// Same outout:
// [1] 100 0xc0000160a8
// [2] 100 0xc0000160a8

เปลี่ยนค่าตัวแปรโดยใช้การ dereference

func main() {
	var price int = 100
	var addr *int = &price
	fmt.Println(price, &price) // 100 0xc0000160a8
 
	*addr = 200                // write
	fmt.Println(price, &price) // 200 0xc0000160a8
}

เนื่องจาก Go ใช้หลักการ Pass by Value คือ copy แล้วส่งค่าไปที่ function หรือ struct

  • p ใน changePrice() จึงเป็นตัวแปรใหม่คนละตัว ที่เพียงได้รับค่าเข้ามา
  • ดังนั้น price จึงไม่ได้ถูกเปลี่ยนค่าจาก changePrice()
func changePrice(p int) {
	p = p - 50
	fmt.Println("[1]", p, &p)
}
 
func main() {
	var price int = 500
	var addr *int = &price
 
	changePrice(price)
	fmt.Println("[2]", price, addr)
}
 
// [1] 450 0xc0000160c0
// [2] 500 0xc0000160a8

Work around: สามารถให้ตัว address เข้าเป็น parameter *int แล้ว dereference pointer เป็น *p จากนั้นส่ง reference &price เข้า changePrice() จะทำให้สามารถเปลี่ยนค่าได้

func changePrice(p *int) {
	*p = *p - 50
	fmt.Println("[1]", p, &p)
}
 
func main() {
	var price int = 500
	var addr *int = &price
 
	changePrice(&price)
	fmt.Println("[2]", price, addr)
}
 
// [1] 0xc000098058 0xc0000ba018
// [2] 450 0xc000098058

สรุปคือถ้า function ไม่ได้ return ค่ามาให้ เราก็สามารถเปลี่ยนแปลงค่าโดยรับ pointer ที่เก็บ address เข้ามาได้

func add1(num int) int {
	return num + 1
}
 
func add2(num int) {
	num = num + 1
}
 
func add3(num *int) {
	*num = *num + 1
}
 
func main() {
	a := add1(10)
	fmt.Println(a)
	// 11 because add1 returns 10 + 1
 
	b := 10
	add2(b)
	fmt.Println(b)
	// 10 because add2 does not change the value of b
 
	c := 10
	add3(&c)
	fmt.Println(c)
	// 11 because add3 changes the value of c through pointer
}

ตัวอย่างการสร้าง method addVote() เพิ่มค่า rating เข้าไปใน slice votes ของ struct movie

type movie struct {
	title       string
	year        int
	rating      float32
	votes       []float64
	genres      []string
	isSuperHero bool
}
 
func (m *movie) addVote(rating float64) {
	m.votes = append(m.votes, rating)
}
 
func main() {
	eg := &movie{
		title:       "Avengers: Endgame",
		year:        2019,
		rating:      8.4,
		votes:       []float64{7, 8, 9, 10},
		genres:      []string{"Action", "Drama"},
		isSuperHero: true,
	}
	fmt.Println("votes:", eg.votes)
 
	eg.addVote(8)
	fmt.Println("votes:", eg.votes)
 
	// votes: [7 8 9 10]
	// votes: [7 8 9 10 8]
}

Maps

Maps เป็น Data Structure มีลักษณะเหมือนกับ Dictionary คือ Key และ Value

  • ตัวอย่างประกาศ maps keys: string และ value: int
var m map[string]int = map[string]int{"a": 1, "b": 2}
fmt.Printf("Values: %#v\n", m)
 
m["c"] = 3 // add
fmt.Printf("Values: %#v\n", m)
 
v1 := m["a"] // get
fmt.Println("Values:", v1)
 
delete(m, "b") // delete
fmt.Printf("Values: %#v\n", m)
 
v2 := m["b"] // get blank return zero value
fmt.Println("Values:", v2)
 
v3, ok := m["b"] // get with ok for check key exist
fmt.Println("Values:", v3, ok)
  • ลองสร้างฟังก์ชัน WordCount เพื่อนับคำซ้ำในประโยค
// WordCount counts the number of times each word occurs in s.
 
func WordCount(s string) map[string]int {
	words := strings.Fields(s)
	r := map[string]int{}
	for _, w := range words {
		r[w] = r[w] + 1
	}
	return r
}
 
func main() {
	s := "If it looks like a duck swims like a duck and quacks like a duck then it probably is a duck"
	w := WordCount(s)
	fmt.Printf("%#v\n", w)
}

Interface

Interface คือรูปแบบลักษณะที่ตกลงร่วมกันและเข้ากันได้ (method signature)

Empty Interface

  • Go v1.8 ขึ้นไป any กับ interface{} เป็นตัวเดียวกัน ใช้เพื่อระบุว่า ใช้ได้กับทุก Type
func main() {
	var v any
 
	v = 36
	fmt.Println(v)
 
	v = "hello"
	fmt.Println(v)
}
  • ใช้ Type assertion เพื่อให้ type ตรงกันกับ function
func show(val int) { // int
	i := val + 2
	fmt.Println(i)
}
 
func main() {
	var v any
	v = 36
	show(v.(int)) // type assertion
}
  • หรือเปลี่ยนให้ type parameter เป็น any ก็จะใช้ได้กับทั้งหมด แต่จะไม่สามารถดำเนินการแบบ int ใน function ได้เหมือนเดิม ต้องทำ Type assertion
  • ใช้ตัวแปร ok เพื่อเช็ค type ก่อน ป้องกันไม่ให้เกิด runtime error ตอน assert type ได้
func show(val any) { // any
	i, ok := val.(int) // type assertion checking
	if ok {
		i = i + 1
		fmt.Println(i)
	} else {
		fmt.Println("Not int")
	}
}
 
func main() {
	var v any
	v = 36
	show(v)
}
  • ใช้ switch case เพื่อจัดการกับแต่ละ Type ได้
func show(val any) {
	switch v := val.(type) { // switch case type
	case int:
		i := v + 1
		fmt.Println("int:", i)
	case string:
		s := v + "Hi"
		fmt.Println("string:", s)
	default:
		fmt.Println("unknown")
	}
}
 
func main() {
	var v any
	v = 36
	show(v)
}

Type Assertion vs Type Conversion ทั้งสอง Concept นี้ ใช้เพื่อแปลงค่าจาก type หนึ่งไปเป็น type ใหม่ แต่มีจุดแตกต่างดังนี้

Type AssertionsType Conversions
Syntaxx.(T)T(value)
Use caseExtract a value of a specific type from an interface valueExplicitly convert a value of one type to another
ResultReturns the value of the asserted type and a boolean indicating whether the assertion succeeded or failedReturns a new value of the specified type
FailureFails if the value being asserted is not of the specified typeFails if the value being converted is not compatible with the target type
PerformanceGenerally slower than type conversions due to additional runtime checksGenerally faster than type assertions because they involve a simple conversion operation

Implementation

  • ประกาศ interface แล้วตั้งชื่อว่า promotion ที่มี method signature ไว้
  • ประกาศ struct ชื่อ course แล้วมี 2 method คือ discount() และ info()
  • จะเห็นว่า course เป็นไปตาม interface promotion เพราะมี method discount() จึงเป็นการ implement อัตโนมัติโดยไม่ต้องเขียน และไม่ได้สนใจ info()
  • ดังนั้น sale() จึงรับ parameter course เข้ามา แล้วสามารถใช้ discount() ได้
  • แต่จะใช้ info() ไม่ได้ เพราะ course เข้ามาใน sale() ในฐานะ interface promotion
type promotion interface {
	discount() float64
}
 
type course struct{}
 
func (c course) discount() float64 {
	return 0.1
}
 
func (c course) info() {
	fmt.Println("Course info", c)
}
 
func sale(val promotion) {
	fmt.Printf("Sale: %#v\n", val.discount())
}
 
func main() {
	v := course{}
	sale(v)
}

Embedding Interface

เราสามารถนำ Interface มาหลอมรวมกันใน Interface ตัวอื่นๆ ได้

  • ประกาศแล้ว interface ตั้งชื่อว่า presenter โดยรวม promotioner และ infoer
  • course เป็นไปตาม interface presenter ที่มีทั้ง method discount() และ info()จึงเป็นการ implement อัตโนมัติ
  • summary() จึงสามารถรับ course เข้ามาได้
type promotioner interface {
	discount() float64
}
 
type infoer interface {
	info()
}
 
type presenter interface { // Embedding Interface
	promotioner
	infoer
}
 
type course struct{}
 
func (c course) discount() float64 {
	return 0.1
}
 
func (c course) info() {
	fmt.Println("Course info", c)
}
 
func summary(val presenter) {
	fmt.Printf("Sale: %#v\n", val.discount())
	val.info()
}
 
func main() {
	v := course{}
	summary(v)
}

Functions

  • กำหนด scope ด้วยปีกกา {curly brackets}
  • สามารถระบุ parameter type ได้
  • สามารถกำหนดการ Return ให้มีมากกว่า 1 ค่า ได้
// No return
func greeting(firstName, lastName string) {
	fmt.Println("Hello", firstName, lastName)
}
 
// Return 1 value
func add(a, b int) int {
	return a + b
}
 
// Return 2 value
func swap(a, b int) (int, int) {
	return b + a
}

First Class Function

Go มองเห็น function เป็นสมาชิกอันดับต้น ๆ คือเป็น variable ได้ โดยมี type เป็น func signature

  • ประกาศเป็นตัวแปร

    var add = func(a, b int) int {
    	return a + b
    }
    fmt.Println(add(1, 2))
  • ประกาศข้างนอก เวลาเอามาใช้ก็เป็นตัวแปรอื่นได้

    func main() {
    	var cal = add
    	fmt.Println(cal(1, 2))
    }
    func add(a, b int) int {
    	return a + b
  • รับ parameter เป็น func ที่กำหนด signature ตรงกันได้

    func add(a, b int) int {
    	return a + b
    }
    func compute(fn func(int, int) int) int {
    	v := fn(3, 4)
    	return v
    }
    func main() {
    	r := compute(add)
    	fmt.Println(r)
    }

Higher Order Function

ฟังก์ชันใด ๆ ที่รับฟังก์ชันเป็น parameter ได้ หรือคืนค่าออกมาเป็นฟังก์ชันได้ Example

func adder() (func() int, func() int) {
	sum := 0
	return func() int {
		sum = sum + 1
		return sum
	}, func() int {
		return sum
	}
}
func main() {
	inc, curr := adder()
	v := inc()
	fmt.Println(v)
	v = inc()
	fmt.Println(v)
	v = curr()
	fmt.Println(v)
}

Closure Function

หมายถึง Higher Order Function ที่เราคืนค่าออกมา โดยมีข้อพิเศษคือสามารถอ้างตัวแปรที่อยู่นอก Scope ของมันได้

  • ตัวอย่างฟังก์ชันชื่อ counterFunc() คืนค่าเป็นฟังก์ชันที่คืนค่า int
  • โดยฟังก์ชันนั้นถูกสร้างอยู่ภายใต้ keyword return แต่สามารถอ้างอิงตัวแปร i ที่อยู่นอก scope ได้ เหมือนเชื่อมตัวมันเองออกไปหาตัวแปรตัวอื่น
  • ผลลัพธ์คือเห็นของชิ้นนั้นเหมือนเป็น state ส่วนตัว ดังนั้นเวลาถ้าเรียกหลายครั้ง จำนวนก็จะเพิ่มขึ้นเพราะ state ของ i ที่เชื่อมอยู่เปลี่ยนไป
func counterFunc() func() int {
	var i int
	return func() int {
	i++
	return i
	}
}
func main() {
	fn := counterFunc()
	fmt.Println(fn()) // 1
	fmt.Println(fn()) // 2
	fmt.Println(fn()) // 3
}

Variadic Function

Variadic Function สามารถถูกเียกใช้งานโดยใช้จำนวน arguments เท่าไหร่ก็ได้

func skills(xs ...string) {
	for _, x := range xs {
		fmt.Println("I am good at", x)
	}
}
 
func main() {
	skills("Java", "Go", "Python")
	// I am good at Java
	// I am good at Go
	// I am good at Python
}

Anonymous Function

เป็นฟังก์ชันที่ไม่ระบุชื่อ มีประโยชน์ในการใช้งานแบบ inline

func main() { 
 
   func(){
      fmt.Println("Welcome! to GeeksforGeeks")
  }()
}

ตัวแปรสามารถรับค่าเป็น anonymous function ได้

func main() { 
 
   value := func(){
      fmt.Println("Welcome! to GeeksforGeeks")
  }
  value()
}

สามารถส่ง arguments ใน anonymous function ได้

func main() {
      
  func(s string){
      fmt.Println(s)
  }("Hello World!")
    
}

Naked Return

เราสามารถคืนค่าโดยไม่ต้องมี argument เรียกว่า  “naked” return โดยกำหนดชื่อไว้ที่ function signature แต่ควรใช้กับ function ที่สั้นและไม่ซับซ้อน

// Naked return
func calculate(a, b int) (sum, product int) {
    sum = a + b
    product = a * b
    return
}
 
// Explicit return
func calculate(a, b int) (int, int) {
    sum := a + b
    product := a * b
    return sum, product
}

Struct

ประกาศชุดกลุ่มข้อมูล

type course struct {
	name       string
	instructor string
	price      float64
}
 
func main() {
	c1 := course{
		name:       "Golang",
		instructor: "John Doe",
		price:      10.99,
	}
	println(c.name)
}

Method

การเรียกใช้ fuction โดยผูกกับ struct หรือ non-struct ก็ได้ โดยอาศัย Reciever

  • การใช้แบบ fuction
func discount(c course, d float64) float64 {
	p := c.price - d
	fmt.Println("Discount:", p)
	return p
}
 
func main() {
	c := course{"Go Fundamentals", "Nigel Poulton", 11.99}
	fmt.Printf("course: %+v\n", c)
 
	d := discount(c, 2.99)
	fmt.Println("discount price:", d)
}
  • การใช้แบบ method
func (c course) discount(d float64) float64 { // Reciever
	p := c.price - d
	fmt.Println("Discount:", p)
	return p
}
 
func main() {
	c := course{"Go Fundamentals", "Nigel Poulton", 11.99}
	fmt.Printf("course: %+v\n", c)
 
	d := c.discount(2.99) // Change
	fmt.Println("discount price:", d)
}

Example: ความแตกต่างระหว่าง struct ที่ return ปกติ กับเป็น pointer

type Person struct {
	Name string
	Age  int
}
 
// Approach 1: Returning Person by value (cannot modify)
func NewPersonImmutable(name string, age int) Person {
	return Person{
		Name: name,
		Age:  age,
	}
}
 
// Approach 2: Returning *Person (can modify)
func NewPersonMutable(name string, age int) *Person {
	return &Person{
		Name: name,
		Age:  age,
	}
}
 
func main() {
	// Approach 1: Immutable
	p1 := NewPersonImmutable("John", 25)
	p1.Age = 30 // Cannot modify directly
 
	// Approach 2: Mutable
	p2 := NewPersonMutable("Jane", 25)
	p2.Age = 30 // Can modify directly
 
	fmt.Println(p1) // Output: {John 25}
	fmt.Println(p2) // Output: &{Jane 30}
}

Example: ตัวอย่าง Student struct ที่มี field University เป็น *University (pointer) แปลว่าการเปลี่ยนแปลงค่าของ instance หนึ่งที่มี reference ถึงของชิ้นเดียวกันจะเปลี่ยนตามไปด้วย

  • student1 กับ student2 มี university ร่วมกัน ถ้าคนใดคนหนึ่งเปลี่ยน อีกคนก็เปลี่ยนตาม
type University struct {
	Name  string
	Location string
}
 
type Student struct {
	Name      string
	Age       int
	University *University
}
 
func main() {
	university := &University{
		Name:     "ABC University",
		Location: "City XYZ",
	}
 
	student1 := Student{
		Name:      "John",
		Age:       20,
		University: university,
	}
 
	student2 := Student{
		Name:      "Alice",
		Age:       22,
		University: university,
	}
 
	student1.University.Name = "XYZ University"
 
	fmt.Println(student1.University.Name) // Output: XYZ University
	fmt.Println(student2.University.Name) // Output: XYZ University
}

Example: ตัวอย่างการ embeded struct ด้วย struct หรือ pointer

package main
 
import "fmt"
 
type Animal struct {
	Name     string
	Sound    string
	IsMammal bool
}
 
type CowA struct {
	Animal
}
 
type CowB struct {
	Animal
}
 
type DuckA struct {
	*Animal
}
 
type DuckB struct {
	*Animal
}
 
func main() {
	cow := Animal{
		Name:     "Cow",
		Sound:    "Moo",
		IsMammal: true,
	}
 
	cowA := CowA{
		Animal: cow,
	}
 
	cowB := CowB{
		Animal: cow,
	}
 
	duck := &Animal{
		Name:     "Duck",
		Sound:    "Quack",
		IsMammal: false,
	}
 
	duckA := DuckA{
		Animal: duck,
	}
 
	duckB := DuckB{
		Animal: duck,
	}
 
	// Modifying the value of the struct
	cowA.Sound = "Mooooooo"
	fmt.Println("cowA:", cowA.Sound) // Mooooooo
	fmt.Println("cowB:", cowB.Sound) // Moo
 
	// Modifying the value of the pointer
	duckA.Sound = "Quackkkkkk"
	fmt.Println("duckA:", duckA.Sound) // Quackkkkkk
	fmt.Println("duckB:", duckB.Sound) // Quackkkkkk
}

Advance

Time

การจัดการ format เวลาใน Go ไม่ได้ใช้ YYYY-MM-DD แต่ใช้เป็นตัวเลข เช่น 02/01/2006 15:04:05

DayMonthYearHourMinuteSecond
02012006150405
func main() {
	now := time.Now()
	fmt.Println(now)
 
	t := time.Date(2019, 11, 17, 20, 34, 58, 0, time.UTC)
	formatTime := t.Format("02/01/2006 15:04:05")
	fmt.Println(formatTime) // 17/11/2019 20:34:58
}

Testing

testing nattrio/go-demo-unit-test Golang มี Unit test มาให้ใช้ภายในตัวอยู่แล้ว ไม่จำเป็นต้องติดตั้งเพิ่ม โดย Convention ในการเขียน Test มี 3 ข้อ ได้แก่

  1. The suffix of a filename is _test.go
  2. The name of the unit test function start with Test
  3. The type of a function with 1 parameter is *testing.T
  4. (optional) A package name can have _test as suffix

สามารถรันเทสโดยใช้คำสั่ง go test

  • go test . ใช้รันเทสใน directory นั้น
  • go test ./… ใช้รันเทสใน directory นั้น รวมถึงโฟลเดอร์ย่อย ๆ ทั้งหมด
  • go test -v ใช้รันเทสใน directory นั้น แสดงแบบละเอียด
  • go test -cover ใช้รันเทสใน directory นั้น แล้วบอก % test coverage
  • go test -run="<test_func/subtest>" ใช้รันเฉพาะซับเทส
  • go test -bench=. ใช้ทดสอบ performance
  • go test -tags=integration ใช้รันเทสตาม tag ที่ระบุไว้

ตัวอย่าง cal.go และ cal_test.go

/* cal.go */
func Add(a, b int) int {
	return a + b
}
 
/* cal_test.go */
import "testing"
 
func TestAdd(t *testing.T) {
	r := Add(1, 2)
 
	if r != 3 {
		t.Error("1 + 2 did not equal 3")
	}
}

หมายเหตุ

  • * (ดอกจัน) หมายถึง pointer แปลว่าเรารับ pointer ของตัวแปรที่มี type เป็น T ซึ่งมาจาก package ชื่อว่า testing
  • เราสามารถวางไฟล์ test กับไฟล์ที่ต้องการ test ไว้ข้างกันหรือใน package ชื่อเดียวกันได้เลย แล้วตั้งชื่อให้ล้อกัน เช่น cal.go กับ cal_test.go

ปกติใน 1 directory จะมี package เดียว แต่ในข้อยกเว้นของการเขียน unit test คือ สามารถแยกไฟล์ test กับไฟล์ที่ต้องการ test ไว้คนละ package ได้ แต่ให้ทำตามเงื่อนไขดังนี้

  • ชื่อข้างหน้าของ package ต้องเหมือนกัน
  • package ที่เป็น test ให้ต่อท้ายด้วย _test
  • เช่น ใน /services มี cal.go กับ cal_test.go
    • cal.go ใช้ package: services
    • cal_test.go ใช้ package: services_test

Unit Test VS Code Configuration in settings.json

"go.coverOnSave": true,
"go.coverOnSingleTest": true,
"go.coverageDecorator": {
    "type": "gutter",
    "coveredHighlightColor": "rgba(64,128,128,0.5)",
    "uncoveredHighlightColor": "rgba(128,64,64,0.25)",        
    "coveredGutterStyle": "blockgreen",
    "uncoveredGutterStyle": "blockred"
}

package สำหรับ test อื่นๆ เช่น testify

Generics

เป็นหลักการ Parametric polymorphism โดย Generics ใช้ในการจัดการการรับค่าข้อมูลต่างชนิดกัน

  • ตัวอย่างเป็นการใช้ min() เพื่อคืนค่าที่น้อยกว่า ถ้าต้องการให้รองรับทั้งชนิด int และ float64 ต้องประกาศ 2 ฟังก์ชันเป็น minInt() และ minFloat64() ทั้งที่มีการเขียนเงื่อนไขข้างในเหมือนกัน
func minInt(a, b int) int {
	if a < b {
		return a
	}
	return b
}
 
func minFloat64(a, b float64) float64 {
	if a < b {
		return a
	}
	return b
}
  • ให้สร้าง interface Number ที่รองรับชนิด int | float64
  • ให้สร้างฟังก์ชัน min[T Number](a, b T) T เพื่อรองรับข้อมูลชนิด T ที่เป็น Number ทำให้สามารถรับข้อมูลเข้าไปได้ทั้ง int และ float64
type Number interface {
	int | float64
}
 
func min[T Number](a, b T) T {
	if a < b {
		return a
	}
	return b
}

Goroutine

  • Go is a concurrent language
  • keyword คือ go วางไว้หน้าฟังก์ชัน เช่น go doSomeThing()
  • Goroutine คือตัวภาษา Go จะทำการสร้าง layer บาง ๆ ขึ้นมา ก่อนจะไปจัดการเรื่อง thread จริง ๆ ใน OS โดยจะมี scheduling ของตนเอง จัดคิวเอง ใช้ทรัพยากรน้อยเมื่อเทียบกับการทำ thread จริง ๆ (มองว่าเป็น lightweight thread ได้)

ตัวอย่างเปรียบเทียบแบบใช้และไม่ใช้ Goroutine จะเห็นว่าถ้าใช้ Goroutine งานจะเสร็จเร็วกว่าเพราะไม่ต้องรอกัน

  • ประกาศ doSomeThing() ใช้จำลองเพื่อหน่วงเวลา
func doSomething() {
	time.Sleep(100 * time.Millisecond)
	fmt.Println("Doing something")
}

รันแบบปกติแล้วจับเวลา

func main() {
	start := time.Now()
	doSomething()
	doSomething()
	doSomething()
	time.Sleep(500 * time.Millisecond)
	fmt.Println(time.Since(start)) // 842.2191ms
}

รันแบบ goroutine แล้วจับเวลา ซึ่งพบว่าเร็วกว่าเพราะไม่มีการรอทำตามลำดับ

func main() {
	start := time.Now()
	go doSomething()
	go doSomething()
	go doSomething()
	time.Sleep(500 * time.Millisecond)
	fmt.Println(time.Since(start)) // 514.1011ms
}

Graceful Shutdown

  • ในการทำงานจริงของ Goroutine จะมีการทำงานพร้อมกันมาก ๆ ในเวลาเดียวกัน
  • ข้อควรระวัง คือ ถ้า main goroutine หยุดการทำงาน (เช่น update service หรือหยุดดื้อ ๆ ) พวก goroutine ต่าง ๆ ที่ถูกปล่อยออกมาก็จะหยุดตามไปด้วย งานที่ทำไว้ยังไม่เสร็จก็หยุด
  • ดังนั้นจึงต้องมีการคำนึงถึง Graceful shutdown เพื่อให้จัดการเรื่องเหล่านี้ ซึ่งอาจเขียนไว้ใน document ของ library ต่าง ๆ

Waitgroup

  • เป็นวิธีการรอของจาก Goroutine กลับมา
  • waitgroup อยู่ใน package sync
  • การใช้คือ ปกติแล้ว main จะไม่รู้ว่า Goroutine แต่ละตัวทำเสร็จหรือเปล่า เลยต้อง sleep รอเผื่อไว้มาก ๆ
  • การใช้ waitgroup จะทำให้เรารู้เวลาที่แต่ละตัวทำเสร็จจริง ๆ

สาธิตการใช้ Waitgroup เพื่อให้ได้เวลาที่ดีที่สุดออกมา โดยไม่ต้องรอเผื่อ

func doSomething(wg *sync.WaitGroup) { // แก้ doSomething ให้รับค่า parameter wg *sync.WaitGroup()
	time.Sleep(100 * time.Millisecond)
	fmt.Println("Doing something")
	wg.Done() // ในฟังก์ชันให้เพิ่ม wg.Done() เพื่อบอกว่างานเสร็จแล้ว
}
 
func main() {
 
	wg := &sync.WaitGroup{} // ประกาศ pointer &sync.WaitGroup() มาใส่ใน wg เพราะต้องมีการเปลี่ยน state
	wg.Add(3)               // เรารู้จำนวน Goroutine ที่แน่ชัดว่ามีเท่าไร ใส่ 3 โดยเปรียบเสมือน counter อันนึง
 
	start := time.Now()
 
	// ส่ง wg เข้าไปในฟังก์ชันทุกตัว
	go doSomething(wg)
	go doSomething(wg)
	go doSomething(wg)
 
	wg.Wait() // ใส่ wg.Wait() แทนที่ Sleep
	fmt.Println(time.Since(start))
}

Race Condition

  • ในการทำงานจริงกับ Concurency ต้องระวังเรื่อง Race Condition
  • Race Condition คือการที่มี Goroutine มากกว่า 1 ตัว พยายามที่จะมา Access ตัวแปรตัวเดียวกัน
  • กรณีนี้เราสามารถ Detect ได้ เกิดเป็น error warinng ว่า data race อาจเกิดขึ้นได้ที่ไหน

Channel

  • Channel เปรียบเสมือนช่องทางที่ใช้สื่อสารกับ Goroutine เช่น Main Goroutine กับ Goroutine หรือ Goroutine กับ Goroutine
  • ความเป็นจริงมันคือการแชร์ memory ไว้ที่นึง
  • Channel ก่อนต้องใช้ Make ก่อน เหมือนกับ Slice และ Map พอเริ่มต้นจะเป็น nil
  • มี keyword คือ chan

Channel มี 2 แบบ ได้แก่

  • Unbufferd
    • การันตี synchronization
    • ถ้ามีคนพยายามส่งของ แต่ไม่มีคนมารับ ของมันจะไม่ไหลไปทางฝั่งคนรับ ของจะค้างอยู่ฝั่งคนส่ง
  • Bufferd
    • มีคนส่งของ ของนั้นจะมายังฝั่งคนรับได้ แม้จะยังไม่มีคนมารับก็ตาม โดยของจะเก้บได้ตามจำนวน buffer ถ้าเต็มแล้วของก็จะส่งมาไม่ได้ จนกว่าของใน buffer จะถูกหยิบไปใช้
    • ทำให้มีโอกาสที่ของจะหายได้ จึงไม่ควรใช้กับของที่มีความสำคัญมาก

2-way Channel

  • Channel ปกติที่ประกาศด้วย chan type จะเป็น 2-way Channel คือสามารถใช้ได้ทั้ง read/write
  • เวลาเอาของใส่เข้าไปให้ใช้ arrow () ถ้าเอาของออกก็ชี้ออก
func demo() {
	rw := make(chan string)
	go greeting(rw)
 
	rw <- "Nattrio"
	fmt.Println(<-rw)
}
 
func greeting(rw chan string) {
	s := <-rw
	rw <- "Hello " + s
}
 
func main() {
	demo() // Hello Nattrio
}

1-way Channel

  • Channel ที่สามารถกำหนดทิศทางได้ โดยใส่ arrow ตอนประกาศ
  • demoOneWay() เป็น read-only แปลว่าของที่คืนมา ใช้อ่านได้อย่างเดียว ไม่สามารถ write ของเข้าไปได้
  • demoWriteChannel() เป็น write-only แปลว่าใช้ write ของเข้าไปได้อย่างเดียว ไม่สามารถ read ออกมาได้
func demoOneWay() <-chan string {
	ch := make(chan string)
	go demoWriteChannel(ch, "Nattrio")
	return ch
}
 
func demoWriteChannel(w chan<- string, name string) {
	w <- "Hello " + name
}

For Each

  • data structure หลายตัวที่สามารถนำมาทำ For Each ได้
  • มี keyword คือ for i := range <data structure>

Range of Array

  • เป็น array เพราะ bucket เป็น […] โดยมันจะคำนวนและแทนที่ตัวเลขในช่วง compile time
  • i ที่ได้จะเป็น index ของ array นั้น เริ่มที่ 0
  • สามารถ access ได้ด้วย a[i]
func rangeIndexOfArray() {
	a := [...]int{1, 2, 3, 4, 5, 6, 7}
	for i := range a {
		fmt.Println(a[i])
	}
}
  • range สามารถคืนค่าที่สองได้ คือ i = index, v = value
func rangeIndexValueOfArray() {
	a := [...]int{1, 2, 3, 4, 5, 6, 7}
	for i, v := range a {
		fmt.Println(i, v)
	}
}

Range of Slice

  • มีลักษณะการใช้งานเหมือนกับ array
func rangeIndexOfSlice() {
	s := []int{1, 2, 3, 4, 5, 6, 7}
	for i := range s {
		fmt.Println(s[i])
	}
}
 
func rangeIndexValueOfSlice() {
	s := []int{1, 2, 3, 4, 5, 6, 7}
	for i, v := range s {
		fmt.Println(i, v)
	}
}

Range of Map

  • มีลักษณะการใช้งานเหมือนกับ array และ slice
func rangeOfMap() {
	m := map[string]int{"a": 1, "b": 2, "c": 3}
	for k, v := range m {
		println(k, v)
	}
}

Range of Chan

  • เวลารับของจาก for range จะได้แค่ค่าเดียว คือค่าที่ไหลเข้ามาใน Channel
  • โดยจะรับค่าไปจนกว่า Channel จะถูก close
  • (ปกติ Channel ใน Go ไม่จำเป็นต้องไป close มัน เราจะ close ก็ต่อเมื่อต้องการส่ง signal เพื่อบอกว่าไม่มีของแล้ว โดยสามารถ close ได้ ทั้งจากฝั่งรับและฝั่งส่ง)
  • (ต้องระวัง ถ้าเกิดไปส่ง close ซ้ำกันมันจะเกิด panic)

Select Statement

  • กรณีที่มี Goroutine หรือ Channel มากกว่าหนึ่งตัวทำงาน แล้วเราไม่ต้องการจะรอทุกตัว คือ เอาแค่ตัวใดตัวหนึ่ง ตัวไหนมาแล้วก็ไม่ให้มัน block จังหวะนั้น เพราะทุกครั้งที่รอ Channel มันจะ ฺBlock ในบรรทัดนั้น โดย Select Statement จะเข้ามาช่วยไม่ให้มัน Block
func main() {
	tick := time.Tick(100 * time.Millisecond)
	boom := time.After(500 * time.Millisecond)
	for {
		select {
		case <-tick:
			fmt.Println("tick.")
		case <-boom:
			fmt.Println("BOOM!")
			return
		default:
			fmt.Println("    .")
			time.Sleep(50 * time.Millisecond)
		}
	}
}

Read more: