สตริงใน Go
ใน Go สตริงโดยพื้นฐานแล้วเป็นลำดับไบต์ที่อ่านอย่างเดียวและไม่สามารถเปลี่ยนแปลงได้ (byte sequence) คำว่า "ลำดับไบต์" ในที่นี้หมายถึงข้อมูลระดับล่างของสตริงประกอบด้วยไบต์ที่เรียงลำดับกัน ไบต์เหล่านี้กินพื้นที่หน่วยความจำต่อเนื่องกัน
ค่าตัวอักษร
ที่กล่าวมาข้างต้นว่าสตริงมีสองวิธีแสดงค่าตัวอักษร แบ่งเป็นสตริงธรรมดาและสตริงดั้งเดิม
สตริงธรรมดา
สตริงธรรมดาแสดงด้วยเครื่องหมายอัญประกาศคู่ "" รองรับอักขระพิเศษ ไม่รองรับการเขียนหลายบรรทัด ด้านล่างเป็นบางสตริงธรรมดา
"นี่คือสตริงธรรมดา\n"
"abcdefghijlmn\nopqrst\t\\uvwxyz"นี่คือสตริงธรรมดา
abcdefghijlmn
opqrst \uvwxyzสตริงดั้งเดิม
สตริงดั้งเดิมแสดงด้วยแบ็กควอต ไม่รองรับอักขระพิเศษ รองรับ การเขียนหลายบรรทัด อักขระทั้งหมดในสตริงดั้งเดิมจะเอาต์พุตตามเดิม รวมถึงการขึ้นบรรทัดใหม่และการเยื้อง
`นี่คือสตริงดั้งเดิม ขึ้นบรรทัดใหม่
เยื้องแท็บ, \tอักขระแท็บแต่ไม่ทำงาน, ขึ้นบรรทัดใหม่
"นี่คือสตริงธรรมดา"
จบ
`นี่คือสตริงดั้งเดิม ขึ้นบรรทัดใหม่
เยื้องแท็บ, \tอักขระแท็บแต่ไม่ทำงาน, ขึ้นบรรทัดใหม่
"นี่คือสตริงธรรมดา"
จบการเข้าถึง
เนื่องจากสตริงโดยพื้นฐานเป็นลำดับไบต์ การดำเนินการดัชนี str[i] ถูกออกแบบให้คืนค่าไบต์ที่ i ไวยากรณ์เหมือนกับสไลซ์ เช่น เข้าถึงองค์ประกอบแรกของสตริง
func main() {
str := "this is a string"
fmt.Println(str[0])
}เอาต์พุตเป็นค่าการเข้ารหัสไบต์ไม่ใช่อักขระ
116ตัดสตริง
func main() {
str := "this is a string"
fmt.Println(string(str[0:4]))
}thisพยายามแก้ไของค์ประกอบสตริง
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)แม้ว่าจะไม่สามารถแก้ไขสตริงได้ แต่สามารถทับได้
func main() {
str := "this is a string"
str = "that is a string"
fmt.Println(str)
}that is a stringการแปลง
สตริงสามารถแปลงเป็นสไลซ์ไบต์ได้ และสไลซ์ไบต์หรือลำดับไบต์ก็สามารถแปลงเป็นสตริงได้ ตัวอย่างดังนี้
func main() {
str := "this is a string"
// แปลงประเภทอย่างชัดเจนเป็นสไลซ์ไบต์
bytes := []byte(str)
fmt.Println(bytes)
// แปลงประเภทอย่างชัดเจนเป็นสตริง
fmt.Println(string(bytes))
}เนื้อหาสตริงเป็นแบบอ่านอย่างเดียวไม่สามารถเปลี่ยนแปลงได้ แต่สไลซ์ไบต์สามารถแก้ไขได้
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 เหมือนกัน
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 ตัวอย่างดังนี้
func main() {
str := "this is a string" // ดูเหมือนความยาวคือ 16
str2 := "นี่คือสตริง" // ดูเหมือนความยาวคือ 7
fmt.Println(len(str), len(str2))
}16 21ดูเหมือนสตริงภาษาจีนสั้นกว่าสตริงภาษาอังกฤษ แต่ความยาวที่หาได้จริงกลับยาวกว่าสตริงภาษาอังกฤษ นี่เป็นเพราะในการเข้ารหัส unicode อักขระภาษาจีนส่วนใหญ่กิน 3 ไบต์ อักขระภาษาอังกฤษกินเพียงหนึ่งไบต์ จากการเอาต์พุตองค์ประกอบแรกของสตริงจะเห็นผลลัพธ์
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
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 ได้ แต่ที่จริงการดำเนินการภายในก็คล้ายกัน
func main() {
var dst, src string
src = "this is a string"
dst = strings.Clone(src)
fmt.Println(src, dst)
}การต่อกัน
การต่อกันของสตริงใช้ตัวดำเนินการ +
func main() {
str := "this is a string"
str = str + " that is a int"
fmt.Println(str)
}ยังสามารถแปลงเป็นสไลซ์ไบต์แล้วเพิ่มองค์ประกอบ
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
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 เป็นสไลซ์ไบต์ที่อ่านอย่างเดียว นั่นคือหน่วยประกอบของสตริงเป็นไบต์ไม่ใช่อักขระ สถานการณ์เช่นนี้มักพบเมื่อวนซ้ำสตริง เช่น โค้ดด้านล่าง
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 ผลลัพธ์จะต่างกัน ดังนี้
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 เช่น โค้ดด้านล่าง
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 แล้ววนซ้ำก็เช่นเดียวกัน ดังนี้
func main() {
str := "hello 世界!"
runes := []rune(str)
for i := 0; i < len(runes); i++ {
fmt.Println(string(runes[i]))
}
}ยังสามารถใช้เครื่องมือในแพ็กเกจ utf8 ได้ เช่น
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 เพื่อดู
