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 versionProject 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", trueConstants
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()
- pointer มีหน้าที่ชี้ไปหา Array จริง ๆ ตัวนึง แปลว่าเบื้องหลัง Slice ทุกตัวจะมี Array อยู่เสมอ
- length เก็บจำนวนสมาชิกที่มันชี้ไป
len() - 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[:]) // AllArguments 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 ยังอยู่ จึงนับได้เป็น 3s2 := 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 0xc0000160a8Work 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 Assertions | Type Conversions | |
|---|---|---|
| Syntax | x.(T) | T(value) |
| Use case | Extract a value of a specific type from an interface value | Explicitly convert a value of one type to another |
| Result | Returns the value of the asserted type and a boolean indicating whether the assertion succeeded or failed | Returns a new value of the specified type |
| Failure | Fails if the value being asserted is not of the specified type | Fails if the value being converted is not compatible with the target type |
| Performance | Generally slower than type conversions due to additional runtime checks | Generally 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
| Day | Month | Year | Hour | Minute | Second |
|---|---|---|---|---|---|
| 02 | 01 | 2006 | 15 | 04 | 05 |
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 ข้อ ได้แก่
- The suffix of a filename is _test.go
- The name of the unit test function start with Test
- The type of a function with 1 parameter is *testing.T
- (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 coveragego test -run="<test_func/subtest>"ใช้รันเฉพาะซับเทสgo test -bench=.ใช้ทดสอบ performancego 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: