string
string Go'da çok yaygın bir temel veri tipidir ve aynı zamanda Go dilinde karşılaştığım ilk veri tipidir.
package main
import "fmt"
func main() {
fmt.Println("hello,world!")
}İnanıyorum ki çoğu insan Go öğrenirken ilk olarak bu kodu yazmıştır. builtin/builtin.go dosyasında, string hakkında basit bir açıklama vardır:
// string, geleneksel olarak UTF-8 kodlanmış metni temsil eden, ancak
// zorunlu olmayan 8-bit baytların tüm dizelerinin kümesidir. Bir dize
// boş olabilir, ancak nil olamaz. string tipinin değerleri değişmezdir.
type string stringBu pasajdan aşağıdaki bilgileri alabiliriz:
string8-bit baytların koleksiyonudurstringtipi genellikleUTF-8kodlamalıdırstringboş olabilir, ancak aslanilolamazstringdeğişmezdir
Bu özellikler sık Go kullananlar için zaten iyi bilinmelidir. Aşağıda farklı şeylere bakalım.
Yapı
Go'da, dizeler runtime'da runtime.stringStruct struct'ı ile temsil edilir, ancak dışa açılmaz. Alternatif olarak, reflect.StringHeader kullanabilirsiniz.
TIP
StringHeader go1.21 sürümünde kullanım dışı bırakılmış olsa da, gerçekten çok sezgisel, bu yüzden aşağıdaki açıklama için hala kullanacağız. Bu anlamayı etkilemez. Ayrıntılar için bkz. Issues · golang/go (github.com).
// runtime/string.go
type stringStruct struct {
str unsafe.Pointer
len int
}
// reflect/value.go
type StringHeader struct {
Data uintptr
Len int
}Alan açıklamaları aşağıdaki gibidir:
Data: dize belleğinin başlangıç adresine işaretçiLen: dizedeki bayt sayısı
Aşağıda unsafe işaretçi ile dize adresine erişim örneği:
func main() {
str := "hello,world!"
h := *((*reflect.StringHeader)(unsafe.Pointer(&str)))
for i := 0; i < h.Len; i++ {
fmt.Printf("%s ", string(*((*byte)(unsafe.Add(unsafe.Pointer(h.Data), uintptr(i)*unsafe.Sizeof(str[0]))))))
}
}Ancak, Go şimdi unsafe.StringData kullanılmasını öneriyor:
func main() {
str := "hello,world!"
ptr := unsafe.Pointer(unsafe.StringData(str))
for i := 0; i < len(str); i++ {
fmt.Printf("%s ", string(*((*byte)(unsafe.Add(ptr, uintptr(i)*unsafe.Sizeof(str[0]))))))
}
}Her iki çıktı da aynıdır:
h e l l o , w o r l d !
Bir dize özünde belleğin bitişik bir bloğudur, her adres bir bayt saklar. Başka bir deyişle, bir bayt dizisidir. len fonksiyonu ile elde edilen sonuç dizedeki karakter sayısı değil, bayt sayısıdır. Bu özellikle dizedeki karakterler ASCII olmayan karakterler olduğunda geçerlidir.
string kendisi çok az bellek kaplar, sadece gerçek veriye bir işaretçi. Bu dizeleri geçirmeyi çok düşük maliyetli yapar. Şahsen düşünüyorum ki, sadece bir bellek referansı tuttuğu için, keyfi olarak değiştirilebilseydi, orijinal referansın hala istenen veriyi içerip içermediğini bilmek zor olurdu (yansıma veya unsafe paketi kullanarak). Başka bir avantajı, doğal olarak eşzamanlılık güvenli olmasıdır; normal koşullar altında kimse değiştiremez.
Birleştirme

Dize birleştirme söz dizimi aşağıdaki gibidir, + operatörü doğrudan kullanılır:
var (
hello = "hello"
dot = ","
world = "world"
last = "!"
)
str := hello + dot + world + lastBirleştirme işlemi runtime'da runtime.concatstrings fonksiyonu tarafından tamamlanır. Aşağıdaki gibi sabit değer birleştirmesi için, derleyici doğrudan sonucu çıkarır:
str := "hello" + "," + "world" + "!"
_ = strAssembly kodunu çıkararak sonucu bilebiliriz. Bir kısmı aşağıdaki gibidir:
LEAQ go:string."hello,world!"(SB), AX
MOVQ AX, main.str(SP)Açıkça, derleyici bunu tam bir dize olarak ele alır, değeri derleme zamanında belirlenir. Runtime'da runtime.concatstrings tarafından birleştirilmeyecektir. Sadece dize değişkenlerini birleştirmek runtime'da tamamlanır. Fonksiyon imzası aşağıdaki gibidir, bir bayt dizisi ve bir dize dilimi alır:
func concatstrings(buf *tmpBuf, a []string) stringBirleştirilen dize değişkenlerinin sayısı 5'ten az olduğunda, bunun yerine aşağıdaki fonksiyonlar kullanılır (kişisel tahmin: parametreler ve anonim değişkenler yığında saklandığı için, runtime'da oluşturulan dilimlerden GC için daha iyi?), yine de concatstrings tarafından tamamlanır:
func concatstring2(buf *tmpBuf, a0, a1 string) string {
return concatstrings(buf, []string{a0, a1})
}
func concatstring3(buf *tmpBuf, a0, a1, a2 string) string {
return concatstrings(buf, []string{a0, a1, a2})
}
func concatstring4(buf *tmpBuf, a0, a1, a2, a3 string) string {
return concatstrings(buf, []string{a0, a1, a2, a3})
}
func concatstring5(buf *tmpBuf, a0, a1, a2, a3, a4 string) string {
return concatstrings(buf, []string{a0, a1, a2, a3, a4})
}concatstrings fonksiyonunun ne yaptığını görelim:
func concatstrings(buf *tmpBuf, a []string) string {
idx := 0
l := 0
count := 0
for i, x := range a {
n := len(x)
// Uzunluk 0 ise atla
if n == 0 {
continue
}
// Sayısal hesaplama taşması
if l+n < l {
throw("string concatenation too long")
}
l += n
// Say
count++
idx = i
}
// Hiç dize yoksa boş dize döndür
if count == 0 {
return ""
}
// Sadece bir dize varsa doğrudan döndür
if count == 1 && (buf != nil || !stringDataOnStack(a[idx])) {
return a[idx]
}
// Yeni dize için bellek tahsis et
s, b := rawstringtmp(buf, l)
for _, x := range a {
// Kopyala
copy(b, x)
// Kısalt
b = b[len(x):]
}
return s
}İlk yaptığı şey, birleştirilecek dizelerin toplam uzunluğunu ve sayısını hesaplamaktır, sonra toplam uzunluğa göre bellek tahsis eder. rawstringtmp fonksiyonu bir dize s ve bayt dilimi b döndürür. Uzunlukları belirlenmiş olsa da, içerikleri yoktur çünkü özünde yeni bellek adreslerine iki işaretçidir. Bellek tahsis kodu aşağıdaki gibidir:
func rawstring(size int) (s string, b []byte) {
// Tip belirtilmemiş
p := mallocgc(uintptr(size), nil, false)
// Bellek tahsis edilmiş olsa da, üzerinde hiçbir şey yok
return unsafe.String((*byte)(p), size), unsafe.Slice((*byte)(p), size)
}Döndürülen dize s kolay temsil içindir ve bayt dilimi b dizeyi değiştirmek için kolaydır. İkisi de aynı bellek adresine işaret eder.
for _, x := range a {
// Kopyala
copy(b, x)
// Kısalt
b = b[len(x):]
}copy fonksiyonu runtime'da runtime.slicecopy çağırır, bu doğrudan belleği src adresinden dst adresine kopyalar. Tüm dizeler kopyalandıktan sonra, birleştirme işlemi tamamlanır. Kopyalanan dize çok büyükse, bu süreç önemli performans tüketir.
Dönüştürme
Daha önce belirtildiği gibi, dizeler kendileri değiştirilemez. Birini değiştirmeye çalışırsanız, derlenmez bile. Go aşağıdaki hatayı verir:
str := "hello" + "," + "world" + "!"
str[0] = '1'cannot assign to string (neither addressable nor a map index expression)Bir dizeyi değiştirmek için, önce bayt dilimi []byte'a dönüştürmeniz gerekir, bu kullanımı çok basittir:
bs := []byte(str)Dahili olarak, runtime.stringtoslicebyte fonksiyonunu çağırır, mantığı çok basittir:
func stringtoslicebyte(buf *tmpBuf, s string) []byte {
var b []byte
if buf != nil && len(s) <= len(buf) {
*buf = tmpBuf{}
b = buf[:len(s)]
} else {
b = rawbyteslice(len(s))
}
copy(b, s)
return b
}Dize uzunluğu tampon uzunluğundan azsa, doğrudan tamponun bayt dilimini döndürür, bu küçük dize dönüştürmeleri için bellek tasarrufu sağlayabilir. Aksi takdirde, dize uzunluğuna eşit bellek tahsis eder, sonra dizeyi yeni bellek adresine kopyalar. rawbyteslice(len(s)) fonksiyonu önceki rawstring fonksiyonuna benzer bir şey yapar, ikisi de bellek tahsis eder.
Benzer şekilde, bayt dilimleri de sözdizimsel olarak kolayca dizelere dönüştürülebilir:
str := string([]byte{'h','e','l','l','o'})Dahili olarak, runtime.slicebytetostring fonksiyonunu çağırır, bu da anlaşılması kolaydır:
func slicebytetostring(buf *tmpBuf, ptr *byte, n int) string {
if n == 0 {
return ""
}
if n == 1 {
p := unsafe.Pointer(&staticuint64s[*ptr])
if goarch.BigEndian {
p = add(p, 7)
}
return unsafe.String((*byte)(p), 1)
}
var p unsafe.Pointer
if buf != nil && n <= len(buf) {
p = unsafe.Pointer(buf)
} else {
p = mallocgc(uintptr(n), nil, false)
}
memmove(p, unsafe.Pointer(ptr), uintptr(n))
return unsafe.String((*byte)(p), n)
}İlk olarak, dilim uzunluğunun 0 veya 1 olduğu özel durumları işler, bellek kopyalama gerekmez. Sonra, tampon uzunluğundan azsa, tampon belleğini kullanır; aksi takdirde, yeni bellek tahsis eder. Son olarak, memmove fonksiyonunu kullanarak doğrudan belleği kopyalar. Kopyalanan belleğin kaynak bellekle hiçbir ilişkisi yoktur, bu yüzden özgürce değiştirilebilir.
Bahsetmeye değer ki, yukarıdaki her iki dönüştürme yöntemi de bellek kopyalaması gerektirir. Kopyalanacak bellek çok büyükse, performans tüketimi de önemli olacaktır. go1.20 sürümüne güncellendiğinde, unsafe paketi aşağıdaki fonksiyonları ekledi:
// Bellek adresine işaret eden tip işaretçisi ve veri uzunluğu geçir, dilim temsilini döndür
func Slice(ptr *ArbitraryType, len IntegerType) []ArbitraryType
// Bir dilim geçir, alttaki dizisine işaretçi al
func SliceData(slice []ArbitraryType) *ArbitraryType
// Geçilen adres ve uzunluğa göre dize döndür
func String(ptr *byte, len IntegerType) string
// Bir dize geçir, başlangıç bellek adresini döndür, ancak döndürülen baytlar değiştirilemez
func StringData(str string) *byteÖzellikle String ve StringData fonksiyonları, bellek kopyalaması içermez ve dönüşümleri tamamlayabilir. Ancak, bunları kullanmanın ön koşulunun verinin salt okunur olduğundan ve daha sonra değiştirilmeyeceğinden emin olmak gerektiği unutulmamalıdır. Aksi takdirde, dize değişir. Aşağıdaki örneğe bakın:
func main() {
bs := []byte("hello,world!")
s := unsafe.String((*byte)(unsafe.SliceData(bs)), len(bs))
bs[0] = 'b'
fmt.Println(s)
}İlk olarak, bayt diliminin alttaki dizi adresini SliceData ile alın, sonra String ile dize temsilini alın. Bayt dilimini doğrudan değiştirdikten sonra, dize de değişir, bu açıkça dizelerin orijinal niyetini ihlal eder. Başka bir örneğe bakalım:
func main() {
str := "hello,world!"
bytes := unsafe.Slice(unsafe.StringData(str), len(str))
fmt.Println(bytes)
// fatal
bytes[0] = 'b'
fmt.Println(str)
}Bir dizenin dilim temsilini aldıktan sonra, bayt dilimini değiştirmeye çalışırsanız, doğrudan fatal olur. Bir dizeyi bildirme şeklini değiştirerek farkı görelim:
func main() {
var str string
fmt.Scanln(&str)
bytes := unsafe.Slice(unsafe.StringData(str), len(str))
fmt.Println(bytes)
bytes[0] = 'b'
fmt.Println(str)
}hello,world!
[104 101 108 108 111 44 119 111 114 108 100 33]
bello,world!Sonuçtan görebileceğiniz gibi, değiştirme gerçekten başarılı oldu. Önceki fatal'ın nedeni, değişken str'in bir dize sabit değeri saklamasıdır. Dize sabit değerleri salt okunur veri segmentinde saklanır, yığın veya yığında değil, bu temel olarak sabit değerlerin daha sonra değiştirilme olasılığını ortadan kaldırır. Normal bir dize değişkeni için, özünde gerçekten değiştirilebilir, ancak derleyici bu söz dizimine izin vermez. Kısacası, dize dönüşümlerini işletmek için unsafe fonksiyonlarını kullanmak güvenli değildir, verinin asla değiştirilmeyeceğini garanti edemediğiniz sürece.
Yineleme
s := "hello world!"
for i, r := range s {
fmt.Println(i, r)
}Çok baytlı karakter durumlarını işlemek için, genellikle dizeler üzerinde yineleme yapmak için for range döngüsü kullanılır. for range kullanarak bir dize üzerinde yineleme yaparken, derleyici bunu derleme sırasında aşağıdaki forma genişletir:
ha := s
for hv1 := 0; hv1 < len(ha); {
hv1t := hv1
hv2 := rune(ha[hv1])
// Tek baytlı karakter olup olmadığını kontrol et
if hv2 < utf8.RuneSelf {
hv1++
} else {
hv2, hv1 = decoderune(ha, hv1)
}
i, r = hv1t, hv2
// Döngü gövdesi
}Genişletilmiş kodda, for range döngüsü klasik bir for döngüsü ile değiştirilir. Döngüde, mevcut baytın tek baytlı karakter olup olmadığını kontrol eder. Çok baytlı karakter ise, tam kodlamasını almak için runtime fonksiyonu runtime.decoderune çağırır, sonra i, r'ye atar. İşledikten sonra, kaynak kodda tanımlanan döngü gövdesini yürütür.
Ara kodu oluşturma işi cmd/compile/internal/walk/range.go içindeki walkRange fonksiyonu tarafından tamamlanır, bu ayrıca for range ile yinelenebilen tüm tipleri işlemekten sorumludur. Bu burada genişletilmeyecektir. Eğer ilgileniyorsanız, kendiniz öğrenebilirsiniz.
