- Published on
Golang Part 1: Go Fundamentals
- Authors
- Name
- Somprasong Damyos
- @somprasongd
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 ตอน
- Go Fundamentals: จะเริ่มจากการศึกษาพื้นฐานภาษา Go ก่อนว่าเขียนยังไง
- Advanced concepts in Go: Advanced concepts ในภาษา Go ที่คิดว่าน่าจะต้องใช้บ่อยๆ
- 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 เลย โดยมีวิธีการดังนี้
- ให้สร้าง folder ชื่อเดียวกันกับ package เช่นต้องการ
package greet
mkdir greet
- แนะนำ ให้สร้างไฟล์แรกของ package โดยใช้ชื่อเดียวกันกับ package ด้วย เพื่อให้รู้ว่านี่คือจุดเริ่มต้นของ package นั้นๆ
cd greet
touch greet.go
- ถ้าต้องการ expose ตัวแปร หรือ function ให้ package อื่น ใช้งานได้ ให้ตั้งชื่อขึ้นต้นด้วยตัวพิมพ์ใหญ่
package greet
func Hi() {
fmt.Println("Hi there 👋")
}
- การเรียกใช้
// ให้ 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 อย่าง คือ
- IF-ELSE เหมือนกับภาษาอื่น เพียงแค่ไม่ต้องใส่
()
os := runtime.GOOS
if os == "darwin" {
fmt.Println("macOS")
} else if os == "linux" {
fmt.Println("Linux")
} else {
fmt.Printf("%s.\n", os)
}
- 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.gopackage 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 ประเภท คือ
- boolean มีค่า zero value คือ
false
bool
- ตัวเลข มีค่า 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
- ตัวเลขจำนวนเชิงซ้อน มีค่า zero value คือ
0
complex64 complex128
- 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("ก")
แทน
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
3626 ส
3623 ว
3633 ั
3626 ส
3604 ด
3637 ี
3594 ช
3634 า
3623 ว
3650 โ
3585 ก
ฟังก์ชันของ 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 แบบ คือ
- Empty Interface รูปแบบ คือ
interface{}
โดยจะรับค่าเป็นอะไรก็ได้ เช่น
var a interface{}
a = 10
a = "ten"
a = true
a = book{}
s := "hello"
a = s
- ถ้าไม่ใช่ 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 ที่คิดว่าจะได้ใช้งานกันว่ามีอะไรบ้าง