Skip to content

สตริงใน Go

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

ค่าตัวอักษร

ที่กล่าวมาข้างต้นว่าสตริงมีสองวิธีแสดงค่าตัวอักษร แบ่งเป็นสตริงธรรมดาและสตริงดั้งเดิม

สตริงธรรมดา

สตริงธรรมดาแสดงด้วยเครื่องหมายอัญประกาศคู่ "" รองรับอักขระพิเศษ ไม่รองรับการเขียนหลายบรรทัด ด้านล่างเป็นบางสตริงธรรมดา

go
"นี่คือสตริงธรรมดา\n"
"abcdefghijlmn\nopqrst\t\\uvwxyz"
นี่คือสตริงธรรมดา
abcdefghijlmn
opqrst  \uvwxyz

สตริงดั้งเดิม

สตริงดั้งเดิมแสดงด้วยแบ็กควอต ไม่รองรับอักขระพิเศษ รองรับ การเขียนหลายบรรทัด อักขระทั้งหมดในสตริงดั้งเดิมจะเอาต์พุตตามเดิม รวมถึงการขึ้นบรรทัดใหม่และการเยื้อง

go
`นี่คือสตริงดั้งเดิม ขึ้นบรรทัดใหม่
  เยื้องแท็บ, \tอักขระแท็บแต่ไม่ทำงาน, ขึ้นบรรทัดใหม่
  "นี่คือสตริงธรรมดา"

  จบ
`
นี่คือสตริงดั้งเดิม ขึ้นบรรทัดใหม่
        เยื้องแท็บ, \tอักขระแท็บแต่ไม่ทำงาน, ขึ้นบรรทัดใหม่
        "นี่คือสตริงธรรมดา"

        จบ

การเข้าถึง

เนื่องจากสตริงโดยพื้นฐานเป็นลำดับไบต์ การดำเนินการดัชนี str[i] ถูกออกแบบให้คืนค่าไบต์ที่ i ไวยากรณ์เหมือนกับสไลซ์ เช่น เข้าถึงองค์ประกอบแรกของสตริง

go
func main() {
   str := "this is a string"
   fmt.Println(str[0])
}

เอาต์พุตเป็นค่าการเข้ารหัสไบต์ไม่ใช่อักขระ

116

ตัดสตริง

go
func main() {
   str := "this is a string"
   fmt.Println(string(str[0:4]))
}
this

พยายามแก้ไของค์ประกอบสตริง

go
func main() {
   str := "this is a string"
   str[0] = 'a' // ไม่สามารถผ่านการคอมไพล์
   fmt.Println(str)
}
main.go:7:2: cannot assign to str[0] (value of type byte)

แม้ว่าจะไม่สามารถแก้ไขสตริงได้ แต่สามารถทับได้

go
func main() {
   str := "this is a string"
   str = "that is a string"
   fmt.Println(str)
}
that is a string

การแปลง

สตริงสามารถแปลงเป็นสไลซ์ไบต์ได้ และสไลซ์ไบต์หรือลำดับไบต์ก็สามารถแปลงเป็นสตริงได้ ตัวอย่างดังนี้

go
func main() {
   str := "this is a string"
   // แปลงประเภทอย่างชัดเจนเป็นสไลซ์ไบต์
   bytes := []byte(str)
   fmt.Println(bytes)
   // แปลงประเภทอย่างชัดเจนเป็นสตริง
   fmt.Println(string(bytes))
}

เนื้อหาสตริงเป็นแบบอ่านอย่างเดียวไม่สามารถเปลี่ยนแปลงได้ แต่สไลซ์ไบต์สามารถแก้ไขได้

go
func main() {
  str := "this is a string"
  fmt.Println(&str)
  bytes := []byte(str)
    // แก้ไขสไลซ์ไบต์
  bytes = append(bytes, 96, 97, 98, 99)
    // กำหนดค่าให้กับสตริงเดิม
  str = string(bytes)
  fmt.Println(str)
}

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

ในสถานการณ์เช่นนี้ หากสตริงหรือสไลซ์ไบต์ที่จะแปลงมีขนาดใหญ่ ค่าใช้จ่ายด้านประสิทธิภาพจะสูงมาก แต่คุณสามารถใช้ไลบรารี unsafe เพื่อทำการแปลงแบบไม่คัดลอกได้ แต่ต้องรับผิดชอบปัญหาความปลอดภัยเบื้องหลังเอง เช่น ในตัวอย่างด้านล่าง ที่อยู่ของ b1 และ s1 เหมือนกัน

go
func main() {
  s1 := "hello world"
  b1 := unsafe.Slice(unsafe.StringData(s1), len(s1))
  fmt.Printf("%p %p", unsafe.StringData(s1), unsafe.SliceData(b1))
}
0xe27bb2 0xe27bb2

ความยาว

ความยาวของสตริง ที่จริงแล้วไม่ใช่จำนวนอักขระ แต่เป็นความยาวของลำดับไบต์ เพียงแต่ส่วนใหญ่เราจัดการกับอักขระ ASCII แต่ละอักขระพอดี用一个ไบต์แสดง ดังนั้นความยาวไบต์กับจำนวนอักขระพอดีเท่ากัน การหาความยาวสตริงใช้ฟังก์ชันในตัว len ตัวอย่างดังนี้

go
func main() {
   str := "this is a string" // ดูเหมือนความยาวคือ 16
   str2 := "นี่คือสตริง" // ดูเหมือนความยาวคือ 7
   fmt.Println(len(str), len(str2))
}
16 21

ดูเหมือนสตริงภาษาจีนสั้นกว่าสตริงภาษาอังกฤษ แต่ความยาวที่หาได้จริงกลับยาวกว่าสตริงภาษาอังกฤษ นี่เป็นเพราะในการเข้ารหัส unicode อักขระภาษาจีนส่วนใหญ่กิน 3 ไบต์ อักขระภาษาอังกฤษกินเพียงหนึ่งไบต์ จากการเอาต์พุตองค์ประกอบแรกของสตริงจะเห็นผลลัพธ์

go
func main() {
   str := "this is a string"
   str2 := "นี่คือสตริง"
   fmt.Println(string(str[0]))
   fmt.Println(string(str2[0]))
   fmt.Println(string(str2[0:3]))
}
t // ตัวอักษร t
è // "เศษ" ของอักขระภาษาจีนตัวหนึ่ง (ไบต์แรก) ของค่าการเข้ารหัส บังเอิญตรงกับค่าการเข้ารหัสของอักขระภาษาอิตาลี è
นี่ // อักขระภาษาจีน

การคัดลอก

คล้ายกับวิธีการคัดลอกอาร์เรย์สไลซ์ การคัดลอกสตริงที่จริงคือการคัดลอกสไลซ์ไบต์ ใช้ฟังก์ชันในตัว copy

go
func main() {
   var dst, src string
   src = "this is a string"
   desBytes := make([]byte, len(src))
   copy(desBytes, src)
   dst = string(desBytes)
   fmt.Println(src, dst)
}

ยังสามารถใช้ฟังก์ชัน strings.clone ได้ แต่ที่จริงการดำเนินการภายในก็คล้ายกัน

go
func main() {
   var dst, src string
   src = "this is a string"
   dst = strings.Clone(src)
   fmt.Println(src, dst)
}

การต่อกัน

การต่อกันของสตริงใช้ตัวดำเนินการ +

go
func main() {
   str := "this is a string"
   str = str + " that is a int"
   fmt.Println(str)
}

ยังสามารถแปลงเป็นสไลซ์ไบต์แล้วเพิ่มองค์ประกอบ

go
func main() {
   str := "this is a string"
   bytes := []byte(str)
   bytes = append(bytes, "that is a int"...)
   str = string(bytes)
   fmt.Println(str)
}

สองวิธีต่อกันข้างต้นประสิทธิภาพแย่มาก โดยทั่วไปสามารถใช้ได้ แต่หากมีข้อกำหนดด้านประสิทธิภาพสูงกว่านี้ สามารถใช้ strings.Builder

go
func main() {
   builder := strings.Builder{}
   builder.WriteString("this is a string ")
   builder.WriteString("that is a int")
   fmt.Println(builder.String())
}
this is a string that is a int

การวนซ้ำ

ที่กล่าวมาข้างต้นแล้วว่า สตริงใน Go เป็นสไลซ์ไบต์ที่อ่านอย่างเดียว นั่นคือหน่วยประกอบของสตริงเป็นไบต์ไม่ใช่อักขระ สถานการณ์เช่นนี้มักพบเมื่อวนซ้ำสตริง เช่น โค้ดด้านล่าง

go
func main() {
  str := "hello world!"
  for i := 0; i < len(str); i++ {
    fmt.Printf("%d,%x,%s\n", str[i], str[i], string(str[i]))
  }
}

ในตัวอย่างเอาต์พุตรูปแบบทศนิยมและรูปแบบสิบหกฐานของไบต์

104,68,h
101,65,e
108,6c,l
108,6c,l
111,6f,o
32,20,
119,77,w
111,6f,o
114,72,r
108,6c,l
100,64,d
33,21,!

เนื่องจากอักขระในตัวอย่าง都属于อักขระ ASCII เพียง用一个ไบต์ก็สามารถแสดงได้ ดังนั้นผลลัพธ์พอดีทุกไบต์ตรงกับหนึ่งอักขระ แต่หากประกอบด้วยอักขระที่ไม่ใช่ ASCII ผลลัพธ์จะต่างกัน ดังนี้

go
func main() {
  str := "hello 世界!"
  for i := 0; i < len(str); i++ {
    fmt.Printf("%d,%x,%s\n", str[i], str[i], string(str[i]))
  }
}

โดยปกติแล้ว อักขระภาษาจีนหนึ่งตัวจะกิน 3 ไบต์ ดังนั้นอาจเห็นผลลัพธ์ดังนี้

104,68,h
101,65,e
108,6c,l
108,6c,l
111,6f,o
32,20,
228,e4,ä
184,b8,¸
150,96,–
231,e7,ç
149,95,•
140,8c,Œ
33,21,!

การวนซ้ำตามไบต์จะแยกอักขระภาษาจีนออก ซึ่งชัดเจนว่าจะเกิดตัวอักษรผิดเพี้ยน สตริงใน Go รองรับ UTF-8 อย่างชัดเจน เพื่อจัดการสถานการณ์เช่นนี้ต้องใช้ประเภท rune เมื่อใช้ for range วนซ้ำ หน่วยการวนซ้ำเริ่มต้นเป็นประเภท rune เช่น โค้ดด้านล่าง

go
func main() {
   str := "hello 世界!"
   for _, r := range str {
      fmt.Printf("%d,%x,%s\n", r, r, string(r))
   }
}

เอาต์พุตดังนี้

104,68,h
101,65,e
108,6c,l
108,6c,l
111,6f,o
32,20,
19990,4e16,世
30028,754c,界
33,21,!

rune โดยพื้นฐานเป็นชื่อประเภท别名ของ int32 ช่วงชุดอักขระ unicode อยู่ที่ 0x0000 - 0x10FFFF สูงสุดเพียงสามไบต์ จำนวนไบต์สูงสุดของการเข้ารหัส UTF-8 ที่ถูกต้องมีเพียง 4 ไบต์ ดังนั้นการใช้ int32 เก็บเป็นเรื่อง理所当然 ในตัวอย่างข้างต้นการแปลงสตริงเป็น []rune แล้ววนซ้ำก็เช่นเดียวกัน ดังนี้

go
func main() {
   str := "hello 世界!"
   runes := []rune(str)
   for i := 0; i < len(runes); i++ {
      fmt.Println(string(runes[i]))
   }
}

ยังสามารถใช้เครื่องมือในแพ็กเกจ utf8 ได้ เช่น

go
func main() {
  str := "hello 世界!"
  for i, w := 0, 0; i < len(str); i += w {
    r, width := utf8.DecodeRuneInString(str[i:])
    fmt.Println(string(r))
    w = width
  }
}

สองตัวอย่างนี้เอาต์พุตเหมือนกัน

TIP

สำหรับรายละเอียดเพิ่มเติมเกี่ยวกับสตริง สามารถไปที่ Strings, bytes, runes and characters in Go เพื่อดู

Golang by www.golangdev.cn edit