Variables และ Types
การประกาศตัวแปร
หัวข้อที่มีชื่อว่า “การประกาศตัวแปร”Go มีสองวิธีในการประกาศตัวแปร:
var declaration — รูปแบบเต็ม สามารถใช้ได้ทั้งใน package level และใน function:
var name string // zero value: ""var age int // zero value: 0var active bool // zero value: falsevar count int = 10 // explicit initial valueShort declaration := — ใช้ได้เฉพาะใน function เหมาะสำหรับใช้งานทั่วไป:
func main() { name := "Alice" // string score := 98.6 // float64 done := false // bool}:= ต้องมีตัวแปรใหม่อย่างน้อยหนึ่งตัวทางซ้าย ถ้าทุกตัวมีอยู่แล้วต้องใช้ = กฎ “ต้องมีตัวใหม่อย่างน้อยหนึ่งตัว” นี่เองที่ทำให้เกิด shadowing โดยไม่ตั้งใจได้ง่าย — ดูหัวข้อด้านล่าง
Zero Values
หัวข้อที่มีชื่อว่า “Zero Values”ตัวแปรที่ประกาศโดยไม่กำหนดค่าจะได้รับ zero value โดยอัตโนมัติ ไม่มี uninitialized memory ใน Go:
| Type | Zero Value |
|---|---|
int, int8, int16, int32, int64 | 0 |
uint, uint8, uint16, uint32, uint64 | 0 |
float32, float64 | 0.0 |
string | "" |
bool | false |
rune (alias สำหรับ int32) | 0 |
byte (alias สำหรับ uint8) | 0 |
array [N]T | array ของ zero value ของ T จำนวน N ตัว |
| struct | ทุก field ตั้งเป็น zero value ของ type ตัวเอง |
interface (รวม error) | nil |
| pointer, slice, map, channel, function | nil |
สิ่งนี้รับประกันว่าคุณใช้ตัวแปรได้อย่างปลอดภัยทันทีหลังประกาศ แม้ไม่มี initializer มีจุดหนึ่งที่ควรเก็บไว้ในใจสำหรับบทต่อไป: nil เองเป็น untyped และ zero value ของตัวแปร interface คือ nil — แต่ pointer ที่เป็น nil และถูกเก็บอยู่ใน interface ไม่ใช่ nil ตัวเดียวกันเสมอไป กับดักนี้อธิบายละเอียดใน Interfaces ตอนนี้แค่รู้ว่า var err error เริ่มต้นเป็น nil จริง ๆ
Types พื้นฐาน
หัวข้อที่มีชื่อว่า “Types พื้นฐาน”Go มี numeric types ที่หลากหลายพร้อมขนาดที่ชัดเจน ส่วน int/uint ที่ไม่ระบุขนาดจะเท่ากับ word size ของ platform (32 หรือ 64 bit ขึ้นกับ GOARCH ไม่ใช่ OS):
var i int = -100 // platform-sized signed integervar u uint = 100 // platform-sized unsigned integervar i32 int32 = 2_147_483_647var f64 float64 = 3.14159var b byte = 255 // alias for uint8var r rune = 'A' // alias for int32; holds a Unicode code pointvar s string = "hello"var ok bool = trueใช้ int เป็น default สำหรับ integers ยกเว้นมีเหตุผลเฉพาะที่ต้องใช้ขนาดอื่น
Constants และ iota
หัวข้อที่มีชื่อว่า “Constants และ iota”Constants ประกาศด้วย const และถูก evaluate ตอน compile time const block ที่มี iota สร้าง sequence เพิ่มค่าอัตโนมัติ — วิธี idiomatic ของ Go สำหรับสร้าง enumeration:
const Pi = 3.14159
type Direction int
const ( North Direction = iota // 0 East // 1 South // 2 West // 3)iota reset กลับเป็นศูนย์ทุกครั้งที่เริ่ม const block ใหม่ และเพิ่มทีละ 1 ในแต่ละบรรทัดของ constant ภายใน block
Untyped Constants และเลขคณิตของ Constant
หัวข้อที่มีชื่อว่า “Untyped Constants และเลขคณิตของ Constant”Constant จะยังไม่มี type ที่แน่นอนจนกว่าจะถูกนำไปใช้ — มันเป็น untyped constant ที่ปรับตัวไปตาม numeric type ที่ context ต้องการ ทำให้ const Pi = 3.14159 ตัวเดียวกันนำไปกำหนดให้ตัวแปร float32 หรือ float64 ได้เลยโดยไม่ต้องแปลง นี่คือเหตุผลที่ const Timeout = 5 * time.Second ใช้ได้ด้วย: 5 เป็น untyped integer constant การคูณเกิดขึ้นตอน compile time และผลลัพธ์ untyped จะกลายเป็น type time.Duration ก็ต่อเมื่อถูก assign เท่านั้น
Untyped constant ถูกคำนวณด้วย precision แบบไม่จำกัดตอน compile time ทำให้เขียนค่าที่เกินขนาดของ type จริงบนเครื่องได้ ตราบใดที่ไม่แปลงมันเป็น type ที่เล็กเกินไป:
const big = 1 << 62 // fine — an untyped constant, not yet any concrete int size_ = int32(big) // compile error: constant 4611686018427387904 overflows int32การ compile บรรทัดที่สองด้วย Go 1.26.5 ให้ error message ตรงตามนี้จริง overflow ถูกจับตอน compile time เพราะ big เป็น untyped — compiler รู้ค่าที่แน่นอนของมันและรู้ว่ามันไม่พอดีกับ int32
iota แบบนอกเหนือจาก +1
หัวข้อที่มีชื่อว่า “iota แบบนอกเหนือจาก +1”iota ไม่ได้จำกัดแค่ “เพิ่มทีละหนึ่ง” — มันเป็นแค่ counter ที่ใช้ใน expression ใดก็ได้ และคุณข้ามค่าบางค่าได้ด้วย blank identifier:
const ( _ = iota // skip 0 KB = 1 << (10 * iota) // 1 << 10 == 1024 MB // 1 << 20 == 1048576 GB // 1 << 30 == 1073741824)แต่ละบรรทัดถัดไปที่ไม่มี expression ชัดเจนจะทำซ้ำ expression ของบรรทัดก่อนหน้า โดยแทนค่า iota ใหม่เข้าไป const ( ... ) block ใหม่จะ reset iota กลับเป็น 0 เสมอ ไม่ว่า block ก่อนหน้าจะจบที่ค่าอะไร
การแปลง Type แบบ Explicit
หัวข้อที่มีชื่อว่า “การแปลง Type แบบ Explicit”Go ไม่มี implicit type conversion คุณต้องแปลงอย่างชัดเจนด้วย T(value):
var x int = 7var y float64 = 2.5
// sum := x + y // compile error: mismatched types int and float64sum := float64(x) + y // explicit conversion: 9.5
var n int = int(sum) // truncates: 9
s := strconv.Itoa(n) // int → stringback, _ := strconv.Atoi(s) // string → int (error ignored with _)Integer Overflow และ uint Underflow
หัวข้อที่มีชื่อว่า “Integer Overflow และ uint Underflow”Fixed-width integer ของ Go wrap รอบตัวเองอย่างเงียบ ๆ เมื่อ overflow — ไม่มี panic ไม่มี error มีแค่ค่าที่เกินขอบเขตไปเฉย ๆ:
var small int8 = 127small++fmt.Println(small) // -128, not a compile error or a panicเหตุการณ์เดียวกันเกิดที่ขอบล่างของ unsigned type ด้วย นี่เป็น bug ที่พบบ่อยเมื่อคำนวณ index จาก len(...): การลบค่าออกจาก unsigned value ที่อาจเป็นศูนย์จะ wrap กลายเป็นเลขบวกมหาศาลแทนที่จะติดลบ:
s := ""var lastIndex uint = uint(len(s)) - 1fmt.Println(lastIndex) // 18446744073709551615 on a 64-bit platform, not -1len เองคืนค่าเป็น int ที่มีเครื่องหมาย ดังนั้น len(s)-1 บน int ก็ได้ -1 ตามปกติ — bug จะปรากฏก็ต่อเมื่อค่านั้นถูกเก็บลง (หรือส่งเป็น) unsigned type เท่านั้น ให้ระวังเมื่อเห็น uint, uint32, หรือ type คล้ายกันโผล่ในเลขคณิตของ size/index/count ที่อยู่ใกล้ศูนย์
String คือ Bytes
หัวข้อที่มีชื่อว่า “String คือ Bytes”String ใน Go คือ sequence ของ byte ที่อ่านได้อย่างเดียว โดยทั่วไป (แต่ไม่เสมอไป) เก็บข้อความแบบ UTF-8 len, การ index ด้วย s[i], และ range ล้วนทำงานกับมัน แต่ไม่ได้เห็นตรงกันว่า “ตัวอักษร” หนึ่งตัวคืออะไร:
len(s)นับ bytes[i]คืนค่า byte ที่ตำแหน่งiไม่ใช่ตัวอักษรrange sคืนค่าเป็น rune (Unicode code point) พร้อม byte offset ที่ rune นั้นเริ่มต้น ซึ่งเป็นเหตุผลที่ลำดับ index มีช่องว่างสำหรับตัวอักษรที่ใช้หลาย byte
word := "héllo" // é encodes to 2 bytes in UTF-8fmt.Println(len(word)) // 6 — byte length, not 5 charactersfmt.Println(word[1]) // 195 — one byte of é's two-byte encoding, not 'é'for i, r := range word { fmt.Printf("byte offset %d: %q\n", i, r)}flowchart TB
subgraph Bytes["len(word) = 6 bytes"]
direction LR
B0["byte 0: 'h'"] --> B1["byte 1: 0xC3"] --> B2["byte 2: 0xA9"] --> B3["byte 3: 'l'"] --> B4["byte 4: 'l'"] --> B5["byte 5: 'o'"]
end
subgraph Runes["range word = 5 runes"]
direction LR
R0["offset 0: 'h'"] --> R1["offset 1: 'é'"] --> R2["offset 3: 'l'"] --> R3["offset 4: 'l'"] --> R4["offset 5: 'o'"]
end ถ้าต้องการจำนวน “ตัวอักษร” จริง ๆ ใช้ utf8.RuneCountInString(s) หรือแปลงด้วย []rune(s) — ทั้งสองแบบใช้เวลา O(n) เพราะ Go ไม่ได้คำนวณจำนวน rune ไว้ล่วงหน้า
Shadowing ด้วย :=
หัวข้อที่มีชื่อว่า “Shadowing ด้วย :=”เพราะ := ต้องการแค่ตัวแปรใหม่ หนึ่งตัว ทางซ้าย มันจึงประกาศตัวแปรใหม่ชื่อซ้ำกับตัวแปรใน scope ภายนอกได้ง่ายโดยไม่ตั้งใจ แทนที่จะใช้ตัวเดิมซ้ำ อันตรายที่สุดคือกับ err ภายใน if:
func run() error { var err error if v, err := step(); err != nil { // := declares a NEW err here, shadowing the outer one fmt.Println("inner saw:", v, err) } return err // the outer err was never touched — still nil}step() ล้มเหลวจริง และ body ของ if ก็ print error ออกมาถูกต้อง — แต่ run() ยัง return nil เพราะ err ที่ถูกตรวจและ print ข้างใน if เป็นคนละตัวกับ err ที่ประกาศด้วย var บรรทัดก่อนหน้าโดยสิ้นเชิง compiler ยอมรับโค้ดนี้แบบเงียบ ๆ เพราะ err ทั้งสองตัวถูกใช้งาน จึงไม่มี “declared and not used” error มาเตือน
go vet ไม่ จับ bug นี้โดย default — shadow check ถูกเอาออกจาก vet ชุดมาตรฐานเพราะมันแจ้งเตือน false positive มากเกินไป ถ้าต้องการจับให้ตั้งใจติดตั้ง analyzer แยกแล้วรันเอง: go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest แล้ว go vet -vettool=$(which shadow) ./... golangci-lint ก็มี setting govet ที่เปิด shadow check เดียวกันได้ แต่นิสัยที่ปลอดภัยกว่าคือหลีกเลี่ยงการใช้ := ใน if/for เมื่อมีตัวแปรชื่อเดียวกันอยู่ใน scope ภายนอกแล้ว — ประกาศด้วย var ไว้ข้างนอก แล้วใช้ = ธรรมดาข้างใน
package main
import "fmt"
type Weekday int
const ( Monday Weekday = iota + 1 Tuesday Wednesday Thursday Friday Saturday Sunday)
func main() { // Zero values var i int var s string var b bool fmt.Println("Zero int:", i) fmt.Println("Zero string:", s == "") fmt.Println("Zero bool:", b)
// Short declaration name := "Go" version := 1.27 fmt.Println("Language:", name, "Version:", version)
// iota enum fmt.Println("Monday =", Monday) fmt.Println("Wednesday =", Wednesday) fmt.Println("Sunday =", Sunday)
// Explicit type conversion meters := 100 kilometers := float64(meters) / 1000.0 fmt.Println("100 meters =", kilometers, "km")
// rune vs byte ch := 'Z' fmt.Println("rune 'Z' as int32:", int32(ch)) fmt.Println("byte value:", byte(ch))} Playground ตัวที่สองด้านล่างรวมทุกจุดในหน้านี้ที่พังแบบเงียบ ๆ ได้: shadowing, overflow, underflow, และ strings-as-bytes
package main
import ( "errors" "fmt" "unicode/utf8")
// step simulates an operation that can fail.func step() (int, error) { return 0, errors.New("boom")}
// run demonstrates := shadowing: the if-statement's err is a NEW variable,// not the outer one declared with var.func run() error { var err error if v, err := step(); err != nil { // shadows the outer err fmt.Println("inner saw:", v, err) } return err // outer err was never touched — still nil}
const ( _ = iota // skip 0 KB = 1 << (10 * iota) // 1 << 10 MB // 1 << 20 GB // 1 << 30)
func main() { // := shadowing: a classic silent bug. fmt.Println("run() returned:", run())
// Silent integer overflow: int8 wraps instead of panicking. var small int8 = 127 small++ fmt.Println("int8(127)+1 =", small)
// uint underflow: subtracting past zero wraps to a huge number. s := "" var lastIndex uint = uint(len(s)) - 1 fmt.Println("uint(len(\"\"))-1 =", lastIndex)
// Strings are bytes: len, indexing, and range disagree on multi-byte runes. word := "héllo" fmt.Println("len(word) =", len(word)) fmt.Println("word[1] =", word[1]) fmt.Println("rune count =", utf8.RuneCountInString(word)) for i, r := range word { fmt.Printf(" byte offset %d: %q\n", i, r) }
// iota beyond +1: bit-shift pattern for size constants. fmt.Println("KB, MB, GB =", KB, MB, GB)} ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| สิ่งที่ได้ | ประโยชน์ | ต้นทุน |
|---|---|---|
| static typing | compiler จับ type mismatch ก่อน runtime | เขียน type annotation มากขึ้น |
| zero values | ไม่มี uninitialized variable, ไม่มี null dereference | nil pointer / nil map ยัง panic ได้ — zero value ปลอดภัยเฉพาะ type ที่ออกแบบให้ใช้ได้ทันที |
| no implicit conversion | ไม่มี hidden precision loss | ต้องเขียน float64(x) ทุกครั้งที่แปลง type |
iota enum | สร้าง enum ได้กระชับ | ไม่มี built-in string representation — ต้องเขียน String() method เอง |
| fixed-width integer | ขนาดและ layout คาดเดาได้ | overflow wrap เงียบ ๆ แทนที่จะ panic — ไม่มี bounds check ในเลขคณิตให้อัตโนมัติ |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “
intมีขนาด 32-bit เสมอ” — ขนาดของintขึ้นกับGOARCH(amd64/arm64 = 64-bit, 386/arm = 32-bit) ไม่ใช่ OS ถ้าต้องการ size คงที่ ใช้int32หรือint64 - “
var x = yกับx := yเหมือนกันเสมอ” —:=ใช้ได้เฉพาะใน function scope เท่านั้น ไม่สามารถใช้ที่ package scope ได้ - “zero value ของ pointer คือ 0” — zero value ของ pointer, slice, map, channel, และ function คือ
nilไม่ใช่ตัวเลข 0 - “integer overflow panic เหมือน nil dereference” — ไม่จริง Go’s fixed-width integer wrap รอบตัวเองอย่างเงียบ ๆ ไม่มีอะไรหยุดตอน runtime มาบอกคุณ
- “
len(s)บอกจำนวนตัวอักษรในs” — มันบอกความยาวเป็น byte string ที่มี rune หลาย byte จะมี byte มากกว่าตัวอักษร
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”โค้ดที่ generate มาซึ่งแตะเรื่อง variable และ basic type มักซ่อน bug พวกนี้ไว้:
if/forheader ใช้:=กับชื่อ (บ่อยครั้งคือerr) ที่มีอยู่แล้วใน scope ภายนอกหรือเปล่า? ตรวจดูว่ามันตั้งใจใช้ตัวแปรเดิมหรือแอบ shadow มันอยู่- เลขคณิตบน fixed-width type (
int8,uint16, byte count, …) เข้าใกล้ค่า minimum/maximum ของ type นั้นหรือเปล่า? ไม่มี overflow check อัตโนมัติ uint(หรือuint32/uint64) ถูกสร้างจากการลบค่าออกจากlen(...)หรือค่าอื่นที่อาจเป็น0หรือเปล่า? นี่คือ bug underflow-กลายเป็นเลขมหาศาลแบบคลาสสิก- โค้ด index string ด้วย
s[i]หรือเทียบlen(s)กับ “จำนวนตัวอักษร” ทั้งที่ string อาจมีข้อความที่ไม่ใช่ ASCII หรือเปล่า? iotablock เริ่มด้วย identifier ธรรมดาแทนที่จะเป็น_ทั้งที่ค่าตัวที่ศูนย์ควรถูกข้ามหรือถือว่า “invalid” หรือเปล่า?
🐛 หา bug ในโค้ดนี้
func step() (int, error) { return 0, errors.New("boom")}
func run() error { var err error if v, err := step(); err != nil { fmt.Println("inner saw:", v, err) } return err}เฉลย
:= ใน if ประกาศ err ตัวใหม่ ที่ scope อยู่แค่ใน if block — มันไม่ได้ assign ให้ err ตัวนอกที่ประกาศด้วย var step() ล้มเหลวจริง และ fmt.Println ข้างใน if ก็ print error จริง แต่ run() ยัง return nil เพราะ err ตัวนอกไม่เคยถูก assign ใหม่ แก้ได้โดยไม่ shadow: ใช้ err ตัวที่ประกาศไว้แล้วด้วยการเขียน var v int; v, err = step() แล้วเช็ค err แยกบรรทัดหลังจากนั้น