Türkçe metinlerle çalışan bir Go programında len("ışık") ifadesinin 4 değil 7 döndürdüğünü ilk gördüğünde şaşırabilirsin. Ya da kullanıcıların şehir adlarını büyük harfe çeviren bir fonksiyonun "istanbul" kelimesini "İSTANBUL" yerine "ISTANBUL" yaptığını fark edersin. İkisi de hata değildir; Go'nun metni nasıl temsil ettiğinin doğal sonuçlarıdır. Bu temsili anladığında hem bu sürprizlerden kurtulur hem de metin işleyen kodu hızlı ve doğru yazarsın.
Go'da bir string, değiştirilemeyen bir byte dizisidir. Bu byte'lar genellikle UTF-8 kodlamasıyla yazılmış metni temsil eder: İngilizce harfler bir byte, Türkçeye özgü ç, ğ, ı, ö, ş, ü harfleri iki byte, bazı semboller üç veya dört byte tutar. Günlük dilde "karakter" dediğimiz birimin Go'daki karşılığı ise rune'dur ve bir Unicode kod noktasını temsil eder. Bir kitabı sayfa sayısıyla, bir cümleyi harf sayısıyla ölçmek gibi, bir string'i de byte sayısıyla veya rune sayısıyla ölçebilirsin. Hangisini kullanacağını bilmek bu dersin ana konusu.
Bu derste string'in bellekteki yapısını, UTF-8 kodlamasını, byte ile rune arasındaki farkı, range ile doğru gezinmeyi ve Türkçe büyük/küçük harf dönüşümünün inceliklerini öğreneceksin. Ardından strings.Builder ile verimli birleştirme yapacak, strings paketinin en çok kullanılan fonksiyonlarını tanıyacak ve strconv ile sayı-metin dönüşümlerini hatalarıyla birlikte ele alacaksın.
String: değişmez bir byte dizisi
String'in iç yapısı
Bir string değişkeni, tıpkı bir dilim gibi küçük bir başlıktan oluşur: byte'ların başladığı adresi gösteren bir işaretçi ve byte cinsinden uzunluk. Kapasite alanı yoktur, çünkü string hiçbir zaman büyümez. 64 bit sistemlerde bu başlık 16 byte'tır. Asıl byte'lar salt okunur bir bellek bölgesinde durur.
s := "Merhaba"
t := s[3:6]
s: ptr ──┐ len=7
▼
┌────┬────┬────┬────┬────┬────┬────┐
│ 4d │ 65 │ 72 │ 68 │ 61 │ 62 │ 61 │ salt okunur byte'lar
└────┴────┴────┴────┴────┴────┴────┘
M e r h a b a
▲
t: ptr ─────────────────┘ len=3 ("hab", kopyalama yok)len(s) string'deki byte sayısını verir. s[i] ifadesi i. byte'ı byte (yani uint8) tipinde döndürür. s[i:j] ise i ile j arasındaki byte'lardan oluşan yeni bir string üretir; dilimlerde olduğu gibi byte'lar kopyalanmaz, yeni başlık aynı belleği gösterir.
package main
import "fmt"
func main() {
s := "Merhaba"
fmt.Println(len(s)) // byte sayısı
fmt.Println(s[0], s[1]) // indeksleme byte (uint8) verir
fmt.Printf("%c %c\n", s[0], s[1])
fmt.Println(s[3:6]) // alt string: byte aralığı
for i := 0; i < len(s); i++ {
fmt.Printf("%02x ", s[i])
}
fmt.Println()
fmt.Printf("%T %T\n", s[0], s[3:6])
}7 77 101 M e hab 4d 65 72 68 61 62 61 uint8 string
s[0] bir harf değil, 77 sayısını döndürdü: M harfinin ASCII kodu. Harf olarak görmek için %c fiilini kullandık. Döngüdeki onaltılık çıktı ise bellekteki byte'ların ta kendisidir. Bu örnekte her harf bir byte olduğu için byte ile harf arasında bire bir eşleşme var; Türkçe harfler işin içine girince bu eşleşme bozulacak.
Değişmezlik
Go'da bir string oluşturulduktan sonra içeriği değiştirilemez. Bir byte'ına atama yapmaya çalışmak derleme hatasıdır:
package main
func main() {
s := "kedi"
s[0] = 'K' // derleme hatası
}./prog.go:5:2: cannot assign to s[0] (neither addressable nor a map index expression)
Değişmezlik ilk bakışta kısıtlayıcı görünür ama önemli avantajlar sağlar. Bir string'i fonksiyona geçirmek veya başka bir değişkene atamak yalnızca 16 byte'lık başlığı kopyalar; kimse içeriği değiştiremeyeceği için byte'ları paylaşmak güvenlidir. Alt string'ler kopyalama yapmadan oluşturulabilir. String'ler map anahtarı olarak kullanılabilir, çünkü anahtarın içeriği map'e eklendikten sonra değişemez. Birden fazla goroutine aynı string'i kilitsiz okuyabilir.
İçeriği değiştirmen gerektiğinde string'i bir []byte dilimine çevirir, dilimi değiştirir ve sonucu yeni bir string'e çevirirsin. İki dönüşüm de byte'ları kopyalar:
package main
import "fmt"
func main() {
s := "kedi"
b := []byte(s) // byte'lar yeni bir diziye kopyalanır
b[0] = 'K'
t := string(b) // yine kopyalanır
fmt.Println(s, t)
u := s // yalnızca başlık kopyalanır, byte'lar paylaşılır
u += "ler" // yeni bir string oluşur, s etkilenmez
fmt.Println(s, u)
}kedi Kedi kedi kediler
u += "ler" satırı u'nun gösterdiği byte'ları değiştirmez. Yeni ve daha uzun bir byte bölgesi ayrılır, "kedi" ile "ler" oraya kopyalanır ve u artık yeni bölgeyi gösterir. s eski bölgeyi göstermeye devam eder.
UTF-8 ve rune kavramı
Unicode ve kod noktaları
Unicode, dünyadaki neredeyse tüm yazı sistemlerindeki her karaktere benzersiz bir numara verir. Bu numaraya kod noktası (code point) denir ve U+ önekiyle onaltılık yazılır: a harfi U+0061, ç harfi U+00E7, ş harfi U+015F, Türk lirası simgesi ₺ ise U+20BA'dır.
Go'da bir kod noktasını tutan tip rune'dur. rune, int32 tipinin takma adıdır; byte de uint8'in takma adıdır. Tek tırnakla yazılan bir karakter literali ('ç') bir rune'dur, çift tırnakla yazılan ("ç") ise bir string:
var b byte = 'a' // 97
var r rune = 'ş' // 351, yani U+015F
var s string = "ş" // 2 byte'lık bir stringUTF-8: değişken uzunluklu kodlama
Kod noktalarını belleğe veya dosyaya yazmanın birkaç yolu vardır; Go'nun kaynak dosyaları ve string literalleri UTF-8 kullanır. UTF-8'de bir kod noktası, büyüklüğüne göre 1 ile 4 byte arasında yer kaplar. ASCII karakterleri tek byte'tır ve bu yüzden ASCII metinler UTF-8 ile birebir aynıdır. Türkçenin özel harfleri iki byte'lık aralığa düşer:
Aşağıdaki program her karakterin kod noktasını ve UTF-8 byte'larını yazdırıyor:
package main
import "fmt"
func main() {
chars := []string{"a", "ç", "ğ", "ı", "ö", "ş", "ü", "İ", "₺", "𝄞"}
for _, s := range chars {
r := []rune(s)[0]
fmt.Printf("%s %-7U %d byte % x\n", s, r, len(s), s)
}
}a U+0061 1 byte 61 ç U+00E7 2 byte c3 a7 ğ U+011F 2 byte c4 9f ı U+0131 2 byte c4 b1 ö U+00F6 2 byte c3 b6 ş U+015F 2 byte c5 9f ü U+00FC 2 byte c3 bc İ U+0130 2 byte c4 b0 ₺ U+20BA 3 byte e2 82 ba 𝄞 U+1D11E 4 byte f0 9d 84 9e
Çıktıdaki % x fiili byte'ları aralarında boşlukla onaltılık yazar. Her Türkçe harfin iki byte'tan oluştuğunu ve bu byte'ların c3, c4 veya c5 ile başladığını görebilirsin. UTF-8'in akıllıca bir özelliği de şudur: Çok byte'lı bir karakterin ilk byte'ı kaç byte süreceğini söyler, devam byte'ları ise her zaman 10 bitleriyle başlar. Böylece bir byte dizisinin ortasından okumaya başlayan bir program bile bir sonraki karakterin nerede başladığını bulabilir.
"çay" string'i bellekte:
byte indeksi: 0 1 2 3
byte değeri: c3 a7 61 79
└─ ç ─┘ a y
rune sayısı: 3 byte sayısı (len): 4len ile utf8.RuneCountInString farkı
len her zaman byte sayısını verir; bu O(1) sürer, çünkü uzunluk başlıkta saklanır. Rune sayısını öğrenmek için unicode/utf8 paketindeki utf8.RuneCountInString fonksiyonunu kullanırsın. Bu fonksiyon byte'ları baştan sona çözümlemek zorunda olduğu için O(n) sürer.
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
words := []string{"Go", "çay", "ışık", "İstanbul", "göğüs", "é"}
for _, w := range words {
fmt.Printf("%-9q len=%-2d rune=%d\n", w, len(w), utf8.RuneCountInString(w))
}
}"Go" len=2 rune=2 "çay" len=4 rune=3 "ışık" len=7 rune=4 "İstanbul" len=9 rune=8 "göğüs" len=8 rune=5 "é" len=3 rune=2
"ışık" dört harflidir ama üç harfi iki byte'lık olduğu için 7 byte tutar. "İstanbul" kelimesinde yalnızca İ iki byte'tır, bu yüzden byte sayısı harf sayısından bir fazladır.
Hangisini ne zaman kullanmalısın? Veritabanı sütunu, ağ protokolü veya dosya boyutu gibi depolama sınırları byte cinsindendir; orada len doğrudur. Kullanıcıya gösterilen "en fazla 20 karakter" gibi kurallarda ise rune sayısı çok daha anlamlıdır.
Son örnek ise önemli bir inceliği gösteriyor: "é", e harfi ile ardından gelen birleştirici vurgu işaretinden (U+0301) oluşur. Ekranda tek bir é olarak görünür ama iki rune'dur. Yani rune da her zaman "kullanıcının gördüğü karakter" değildir; bazı emojiler ve bayraklar gibi birden çok rune'un birleşerek tek bir sembol oluşturduğu durumlar vardır. Türkçe metinlerin büyük çoğunluğunda harfler tek rune olarak kodlandığı için rune sayısı pratikte iyi bir yaklaşımdır. Birleşik karakterleri de doğru saymak gerekiyorsa standart kütüphanenin dışındaki Unicode segmentasyon kütüphanelerine başvurmak gerekir.
İndeksleme byte verir, range rune verir
İndeksleme ve dilimleme tuzağı
s[i] her zaman bir byte döndürdüğü için, çok byte'lı karakterlerde indeksleme sana harfin kendisini değil onun bir parçasını verir. Byte sınırlarını gözetmeden dilimlemek de karakterleri ortadan böler:
package main
import "fmt"
func main() {
s := "çay"
fmt.Println(s[0], s[1]) // ç'nin iki byte'ı
fmt.Printf("%c\n", s[0]) // 0xC3 tek başına bir harf gibi yorumlanır
fmt.Printf("%q\n", s[:1]) // yarım karakter: geçersiz UTF-8
fmt.Println(s[:2], s[2:]) // doğru sınırdan bölmek
}195 167 Ã "\xc3" ç ay
s[0] değeri 195'tir, yani 0xC3. Bu sayıyı %c ile yazdırdığında Go onu U+00C3 kod noktası olarak yorumlar ve ekrana à basar; ç ile hiçbir ilgisi yoktur. s[:1] ise tek başına geçerli bir UTF-8 dizisi olmayan tek bir byte içerir ve %q bunu "\xc3" olarak gösterir. Böyle yarım karakterler ekrana veya bir dosyaya yazıldığında bozuk semboller olarak görünür. Metni karakter bazında işlemek istediğinde bir sonraki iki araca başvurursun: range ve []rune.
range ile rune gezinme
for i, r := range s döngüsü string'i rune rune gezer. r çözümlenmiş rune'dur; i ise o rune'un başladığı byte indeksidir. Bu yüzden Türkçe harflerden sonra indeksler birer birer değil, ikişer atlar:
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
s := "Gökçe"
for i, r := range s {
fmt.Printf("indeks=%d rune=%c kod=%U byte=%d\n", i, r, r, utf8.RuneLen(r))
}
bad := "a\xffb" // ortasında geçersiz bir byte var
for i, r := range bad {
fmt.Printf("%d:%q ", i, r)
}
fmt.Println()
fmt.Println(utf8.ValidString(s), utf8.ValidString(bad))
}indeks=0 rune=G kod=U+0047 byte=1 indeks=1 rune=ö kod=U+00F6 byte=2 indeks=3 rune=k kod=U+006B byte=1 indeks=4 rune=ç kod=U+00E7 byte=2 indeks=6 rune=e kod=U+0065 byte=1 0:'a' 1:'�' 2:'b' true false
s := "Gökçe"
byte indeksi: 0 1 2 3 4 5 6
byte değeri: 47 c3 b6 6b c3 a7 65
G └─ ö ─┘ k └─ ç ─┘ e
range i: 0 1 3 4 6range geçersiz bir byte ile karşılaştığında döngüyü bozmaz; o byte için Unicode'un "yerine geçen karakteri" olan U+FFFD'yi (�) verir ve bir byte ilerler. Dışarıdan gelen verinin geçerli UTF-8 olup olmadığını önceden kontrol etmek istersen utf8.ValidString kullanabilirsin.
range ile gezinme yeni bellek ayırmaz ve O(n) sürer. Karakterleri sırayla işlemek için (saymak, filtrelemek, dönüştürmek) en verimli yol budur.
[]rune dönüşümü
Karakterlere indeksle rastgele erişim gerekiyorsa, örneğin "3. harf nedir?" sorusunu çok kez soracaksan, string'i []rune dilimine çevirirsin. Bu dönüşüm tüm string'i çözümler ve her rune için 4 byte'lık yer ayırır. Sonrasında runes[i] gerçekten i. karakterdir:
package main
import "fmt"
func main() {
s := "Gökçe"
runes := []rune(s) // O(n) çözümleme + yeni dizi
fmt.Println(len(s), len(runes))
fmt.Printf("%c %c\n", runes[1], runes[3])
runes[0] = 'K' // dilim değiştirilebilir
fmt.Println(string(runes), s)
fmt.Println(string(runes[1:4])) // karakter bazında alt string
fmt.Println(runes) // kod noktaları
}7 5 ö ç Kökçe Gökçe ökç [75 246 107 231 101]
[]rune ile çalışmak rahattır ama bedeli vardır: Dönüşüm O(n) sürer ve ASCII ağırlıklı bir metinde bellek kullanımını dört katına çıkarır. Yalnızca sırayla gezmek yetiyorsa range kullan; indeksle erişim veya karakterlerin yerini değiştirme gerektiğinde []rune'a geç. Tek bir rune'u string'e çevirmek için string(r) yazabilirsin.
Stringi doğru ters çevirmek
String ters çevirme, byte ile rune farkını görmenin en iyi yoludur. Byte'ları ters çevirmek çok byte'lı karakterlerin byte sırasını da ters çevirir ve geçersiz UTF-8 üretir:
package main
import "fmt"
func reverseBytes(s string) string {
b := []byte(s)
for i, j := 0, len(b)-1; i < j; i, j = i+1, j-1 {
b[i], b[j] = b[j], b[i]
}
return string(b)
}
func reverseRunes(s string) string {
r := []rune(s)
for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r)
}
func main() {
for _, s := range []string{"Gopher", "çiçek", "Ağaç"} {
fmt.Printf("%-8q byte: %-22q rune: %q\n", s, reverseBytes(s), reverseRunes(s))
}
}"Gopher" byte: "rehpoG" rune: "rehpoG" "çiçek" byte: "ke\xa7\xc3i\xa7\xc3" rune: "keçiç" "Ağaç" byte: "\xa7\xc3a\x9f\xc4A" rune: "çağA"
"Gopher" gibi saf ASCII bir kelimede iki yöntem aynı sonucu verir; bu yüzden byte tabanlı hata yalnızca ASCII ile test edilen kodda gözden kaçar. "çiçek" kelimesinde ise ç'nin iki byte'ı a7 c3 sırasına döner ve anlamsız bir diziye dönüşür. Rune tabanlı sürüm doğru sonucu verir. Tek sınırlaması, birleştirici işaretler içeren metinlerde işareti bağlı olduğu harften ayırabilmesidir. Türkçe metinlerde bu durum nadirdir.
Türkçe büyük/küçük harf dönüşümü
Noktalı ve noktasız i problemi
Çoğu Latin alfabesinde i harfinin büyüğü I, I harfinin küçüğü de i'dir. Türkçede ise iki ayrı harf çifti vardır: i ↔ İ ve ı ↔ I. Unicode'un varsayılan büyük/küçük harf eşlemesi Türkçeye göre değil, genel kurala göre tanımlanmıştır. strings.ToUpper ve strings.ToLower da bu varsayılan eşlemeyi kullanır. Türkçe kuralları uygulamak için unicode.TurkishCase özel eşlemesini strings.ToUpperSpecial ve strings.ToLowerSpecial fonksiyonlarına verirsin:
package main
import (
"fmt"
"strings"
"unicode"
)
func main() {
fmt.Println(strings.ToUpper("istanbul"))
fmt.Println(strings.ToUpperSpecial(unicode.TurkishCase, "istanbul"))
fmt.Println(strings.ToLower("ISPARTA"))
fmt.Println(strings.ToLowerSpecial(unicode.TurkishCase, "ISPARTA"))
// ı ve İ harflerinde iki yöntem aynı sonucu verir
fmt.Println(strings.ToUpper("ılık"), strings.ToUpperSpecial(unicode.TurkishCase, "ılık"))
fmt.Println(strings.ToLower("İZMİR"), strings.ToLowerSpecial(unicode.TurkishCase, "İZMİR"))
// Tek bir rune için
fmt.Printf("%c %c\n", unicode.ToUpper('i'), unicode.TurkishCase.ToUpper('i'))
fmt.Printf("%c %c\n", unicode.ToLower('I'), unicode.TurkishCase.ToLower('I'))
}ISTANBUL İSTANBUL isparta ısparta ILIK ILIK izmir izmir I İ i ı
Tablodaki "yanlış" işaretleri yalnızca Türkçe metin için geçerlidir. Aynı program İngilizce veya Almanca bir metni işliyorsa varsayılan eşleme doğrudur ve TurkishCase kullanmak orada hataya yol açar: İngilizce "file" kelimesi "FİLE" olur. Bu yüzden hangi dönüşümün doğru olduğu metne değil, metnin dilinin bilinmesine bağlıdır. Kullanıcıdan gelen Türkçe arama sorgularını, şehir ve kişi adlarını normalleştirirken TurkishCase kullan. Protokol anahtar kelimeleri, dosya uzantıları ve HTTP başlıkları gibi dilden bağımsız tanımlayıcılarda ise standart fonksiyonlar doğrudur.
Karşılaştırma ve sıralama da dile bağlıdır
Büyük/küçük harf duyarsız karşılaştırma yapan strings.EqualFold da dilden bağımsız Unicode kurallarını uygular. Türkçe için basit ve doğru bir yaklaşım, iki metni TurkishCase ile küçültüp karşılaştırmaktır:
package main
import (
"fmt"
"slices"
"strings"
"unicode"
)
func equalFoldTR(a, b string) bool {
return strings.ToLowerSpecial(unicode.TurkishCase, a) ==
strings.ToLowerSpecial(unicode.TurkishCase, b)
}
func main() {
fmt.Println(strings.EqualFold("İstanbul", "istanbul"), equalFoldTR("İstanbul", "istanbul"))
fmt.Println(strings.EqualFold("ISPARTA", "ısparta"), equalFoldTR("ISPARTA", "ısparta"))
fmt.Println(strings.EqualFold("IRMAK", "irmak"), equalFoldTR("IRMAK", "irmak"))
words := []string{"şemsiye", "çilek", "dut", "ördek", "cam", "otel", "ızgara", "incir", "su"}
slices.Sort(words)
fmt.Println(words)
}false true false true true false [cam dut incir otel su çilek ördek ızgara şemsiye]
İlk iki satırda EqualFold Türkçede aynı olan kelimeleri farklı saydı; üçüncü satırda ise Türkçede farklı olan "IRMAK" (ırmak) ile "irmak" kelimelerini aynı saydı. equalFoldTR üç durumu da doğru değerlendirdi.
Sıralama sonucu da Türkçe alfabeye uymuyor. slices.Sort string'leri byte byte karşılaştırır. ç, ö, ı ve ş harflerinin UTF-8 byte'ları tüm ASCII harflerinden büyük olduğu için bu harflerle başlayan kelimeler en sona düştü. Türkçe alfabetik sıra ise "cam, çilek, dut, ızgara, incir, otel, ördek, su, şemsiye" olmalıydı.
strings.Builder ile verimli birleştirme
String'ler değişmez olduğu için s += "x" her seferinde yeni bir string ayırır ve eski içeriğin tamamını oraya kopyalar. Döngü içinde bu, fark edilmeden karesel maliyete dönüşür:
s += "ab" (her adımda eski içerik baştan kopyalanır)
1. adım: "" + "ab" → "ab" 2 byte kopyala
2. adım: "ab" + "ab" → "abab" 4 byte kopyala
3. adım: "abab" + "ab" → "ababab" 6 byte kopyala
...
n. adım: 2n byte kopyala
toplam: 2 + 4 + ... + 2n ≈ n² byte → O(n²)strings.Builder bu sorunu çözer. İçinde büyüyebilen bir byte dilimi tutar; her yazma işlemi bu dilime append yapar ve kapasite gerektiğinde katlanarak büyür. Sonuç olarak n parça eklemek toplamda O(n) sürer. String() metodu biriktirilen byte'ları kopyalamadan bir string olarak döndürür.
package main
import (
"fmt"
"strings"
)
func main() {
items := []string{"elma", "armut", "kiraz"}
prices := []float64{12.5, 20, 45.75}
var b strings.Builder // sıfır değeri kullanıma hazır
b.Grow(128) // isteğe bağlı: tahmini boyutu baştan ayır
b.WriteString("Sepet\n")
total := 0.0
for i, item := range items {
fmt.Fprintf(&b, "%d) %-6s %6.2f TL\n", i+1, item, prices[i])
total += prices[i]
}
b.WriteString(strings.Repeat("-", 20))
b.WriteByte('\n')
b.WriteRune('₺')
fmt.Fprintf(&b, " toplam: %.2f", total)
fmt.Println(b.String())
fmt.Println("uzunluk:", b.Len(), "byte")
}Sepet 1) elma 12.50 TL 2) armut 20.00 TL 3) kiraz 45.75 TL -------------------- ₺ toplam: 78.25 uzunluk: 104 byte
WriteString, WriteByte ve WriteRune farklı türde parçalar eklemeni sağlar. fmt.Fprintf(&b, ...) ise biçimlendirilmiş metni doğrudan builder'a yazar, çünkü *strings.Builder bir io.Writer'dır (bu arayüzü Arayüzler dersinde göreceksin). Grow ile kapasiteyi baştan ayırmak zorunlu değildir ama son boyutu kabaca biliyorsan ara büyümeleri önler.
Elinde zaten bir string dilimi varsa ve aralarına bir ayırıcı koyacaksan strings.Join en kısa ve verimli yoldur; toplam boyutu önceden hesaplayıp tek bir ayırma yapar. Birkaç parçayı bir kez birleştirmek için + kullanmakta da sakınca yoktur. Sorun yalnızca döngü içinde biriktirmededir.
strings paketi
strings paketi metin işlemenin günlük ihtiyaçlarını karşılar. Tüm fonksiyonlar değişmezlik gereği girdiyi değiştirmez, yeni bir değer döndürür. Konum döndüren fonksiyonların (Index gibi) sonucunun byte indeksi olduğunu unutma.
Arama
package main
import (
"fmt"
"strings"
)
func main() {
s := "Go ile veri yapıları ve algoritmalar öğreniyorum"
fmt.Println(strings.Contains(s, "veri"), strings.Contains(s, "Veri"))
fmt.Println(strings.ContainsAny(s, "xwq"), strings.ContainsRune(s, 'ğ'))
fmt.Println(strings.HasPrefix(s, "Go"), strings.HasSuffix(s, "rum"))
fmt.Println(strings.Index(s, "ve"), strings.LastIndex(s, "ve"))
fmt.Println(strings.Index(s, "öğren"), strings.Index(s, "Python"))
fmt.Println(strings.Count(s, "a"))
}true false false true true true 7 23 39 -1 5
Contains büyük/küçük harfe duyarlıdır. Index bulamadığında -1 döndürür. "öğren" kelimesinin byte indeksi, harf sırasından büyüktür, çünkü önündeki "yapıları" kelimesinde iki tane iki byte'lık ı vardır. Bu indeksi s[i:] gibi dilimlemede kullanmak güvenlidir, çünkü Index her zaman bir karakterin başlangıcını gösterir.
Bölme ve birleştirme
package main
import (
"fmt"
"strings"
)
func main() {
csv := "Ayşe,Mert,,Zeynep"
fmt.Printf("%q\n", strings.Split(csv, ","))
fmt.Printf("%q\n", strings.SplitN(csv, ",", 2))
messy := " Go ile\tkod\nyaz "
fmt.Printf("%q\n", strings.Split(messy, " "))
fmt.Printf("%q\n", strings.Fields(messy))
fmt.Println(strings.Join([]string{"2026", "09", "11"}, "-"))
key, value, found := strings.Cut("dil=tr", "=")
fmt.Println(key, value, found)
fmt.Println(len(strings.Split("", ",")))
}["Ayşe" "Mert" "" "Zeynep"] ["Ayşe" "Mert,,Zeynep"] ["" "" "Go" "" "" "ile\tkod\nyaz" "" ""] ["Go" "ile" "kod" "yaz"] 2026-09-11 dil tr true 1
Split ayırıcıyı tam olarak gördüğü her yerde böler; art arda gelen ayırıcılar arasında boş string'ler oluşur. Bu davranış CSV gibi boş alanların anlamlı olduğu formatlarda istenen şeydir. Fields ise bir veya daha fazla boşluk karakterini (boşluk, sekme, satır sonu) tek ayırıcı sayar ve baştaki ve sondaki boşlukları atar; kullanıcı girdisini kelimelere ayırmak için idealdir. Cut, "anahtar=değer" gibi tek bir ayırıcıyla ikiye bölme işini tek satırda yapar. Son satırdaki sonuca dikkat et: Boş bir string'i bölmek boş bir dilim değil, tek elemanlı [""] dilimini döndürür.
Değiştirme ve kırpma
package main
import (
"fmt"
"strings"
)
func main() {
s := "merhaba dünya, merhaba Go"
fmt.Println(strings.Replace(s, "merhaba", "selam", 1))
fmt.Println(strings.ReplaceAll(s, "merhaba", "selam"))
fmt.Printf("%q\n", strings.TrimSpace(" \t boşluklar gitti \n"))
fmt.Println(strings.Trim("***indirim***", "*"))
fmt.Println(strings.TrimPrefix("fatura_2026.pdf", "fatura_"))
fmt.Println(strings.TrimSuffix("fatura_2026.pdf", ".pdf"))
fmt.Println(strings.Repeat("=", 12))
// Her rune'a bir fonksiyon uygula; negatif dönen rune silinir
noVowels := strings.Map(func(r rune) rune {
if strings.ContainsRune("aeıioöuü", r) {
return -1
}
return r
}, "gökyüzü")
fmt.Println(noVowels)
}selam dünya, merhaba Go selam dünya, selam Go "boşluklar gitti" indirim 2026.pdf fatura_2026 ============ gkyz
Replace'in son argümanı en fazla kaç değişiklik yapılacağını belirler; -1 vermek ReplaceAll ile aynıdır. Trim ikinci argümandaki karakterlerin herhangi birini iki uçtan siler, bir alt string'i değil. Bir önek veya soneki tam olarak silmek için TrimPrefix ve TrimSuffix kullanılır. strings.Map ise her rune'u verdiğin fonksiyondan geçirir ve bu örnekte olduğu gibi karakter bazında dönüşümler için kullanışlıdır.
strconv: sayılar ve metinler
Kullanıcı girdisi, dosyalar, komut satırı argümanları ve HTTP parametreleri sana her zaman metin olarak gelir. Bu metinleri sayıya, sayıları da metne çevirmek için strconv paketi kullanılır. Bu işlemi fmt.Sprintf ile de yapabilirsin, ama strconv hem daha hızlıdır hem de ayrıştırma hatalarını açıkça bildirir.
Atoi ve Itoa
package main
import (
"fmt"
"strconv"
)
func main() {
// string -> int
n, err := strconv.Atoi("42")
fmt.Println(n+1, err)
for _, input := range []string{"12a", "", " 7", "99999999999999999999"} {
n, err := strconv.Atoi(input)
if err != nil {
fmt.Println("hata:", err, "| n =", n)
continue
}
fmt.Println("sayı:", n)
}
// int -> string
s := strconv.Itoa(2026)
fmt.Println(s+" yılı", len(s))
}43 <nil> hata: strconv.Atoi: parsing "12a": invalid syntax | n = 0 hata: strconv.Atoi: parsing "": invalid syntax | n = 0 hata: strconv.Atoi: parsing " 7": invalid syntax | n = 0 hata: strconv.Atoi: parsing "99999999999999999999": value out of range | n = 9223372036854775807 2026 yılı 4
strconv.Atoi ("ASCII to integer") iki değer döndürür: sayı ve bir hata. Hata nil değilse sayıyı kullanmamalısın. Çıktıda görüldüğü gibi başarısız ayrıştırmada n genellikle 0'dır, ama aralık dışı bir sayıda int'in en büyük değeri döner. Hata mesajları da bilgilendiricidir: invalid syntax metnin sayı biçiminde olmadığını, value out of range ise sayının tipe sığmadığını söyler. Baştaki boşluk da geçersiz sayılır; kullanıcı girdisini önce strings.TrimSpace ile temizlemek iyi bir alışkanlıktır. Hataları nasıl sınıflandıracağını ve sarmalayacağını Hata Yönetimi dersinde göreceksin.
strconv.Itoa ise ters yönde çalışır ve hata döndürmez, çünkü her tamsayının bir metin karşılığı vardır.
ParseFloat, FormatInt, ParseBool ve Quote
package main
import (
"fmt"
"strconv"
"strings"
)
func main() {
f, err := strconv.ParseFloat("3.14", 64)
fmt.Println(f, err)
_, err = strconv.ParseFloat("3,14", 64) // Türkçe ondalık virgül
fmt.Println(err)
f, _ = strconv.ParseFloat(strings.Replace("3,14", ",", ".", 1), 64)
fmt.Println(f)
fmt.Println(strconv.FormatFloat(2.0/3, 'f', 2, 64))
// Farklı tabanlar
fmt.Println(strconv.FormatInt(255, 2), strconv.FormatInt(255, 16), strconv.FormatInt(255, 36))
v, err := strconv.ParseInt("ff", 16, 64)
fmt.Println(v, err)
_, err = strconv.ParseInt("300", 10, 8) // int8'e sığmaz
fmt.Println(err)
b, err := strconv.ParseBool("true")
fmt.Println(b, err)
// Go sözdiziminde tırnaklı metin
fmt.Println(strconv.Quote("sekme\tve \"tırnak\"\n"))
fmt.Println(strconv.QuoteToASCII("çay"))
u, err := strconv.Unquote(`"satır\tsonu"`)
fmt.Println(u, err)
}3.14 <nil> strconv.ParseFloat: parsing "3,14": invalid syntax 3.14 0.67 11111111 ff 73 255 <nil> strconv.ParseInt: parsing "300": value out of range true <nil> "sekme\tve \"tırnak\"\n" "\u00e7ay" satır sonu <nil>
Türkiye'de ondalık ayırıcı olarak virgül kullanıldığı için kullanıcılar sık sık "3,14" yazar. ParseFloat ise yalnızca Go'nun sayı sözdizimini, yani noktayı kabul eder. Bu yüzden virgülü noktaya çevirmek yaygın bir çözümdür. Ancak "1.250,50" gibi binlik ayırıcı içeren girdilerde basit bir değiştirme yanlış sonuç verir; biçimi net olmayan girdilerde kullanıcıya hangi biçimi beklediğini açıkça söylemek en güvenli yoldur.
FormatInt(sayı, taban) bir tamsayıyı 2 ile 36 arasındaki herhangi bir tabanda yazar; ParseInt(metin, taban, bitBoyutu) ise tersini yapar ve sonucun bitBoyutu bitlik bir tipe sığmasını kontrol eder. FormatFloat'ın 'f' biçimi ve 2 hassasiyeti, virgülden sonra iki basamak üretir.
strconv.Quote bir string'i Go kaynak kodunda yazıldığı biçimde, çift tırnak içinde ve kaçış dizileriyle döndürür. Görünmeyen karakterleri (sekme, satır sonu) log'larda görünür kılmak için çok işe yarar. fmt paketindeki %q fiili de aynı işi yapar. QuoteToASCII ASCII dışındaki karakterleri ç gibi kaçış dizilerine çevirir; Unquote ise tırnaklı biçimi çözerek asıl metne geri döndürür.
Sık yapılan hatalar
- Karakter sayısını
lenile ölçmek.lenbyte sayısıdır; Türkçe metinde harf sayısından büyük olur. Kullanıcıya dönük sınırlar içinutf8.RuneCountInStringkullan. s[i]ile i. harfe erişmeye çalışmak. İndeksleme byte döndürür. Harfleri sırayla işlemek içinfor _, r := range s, rastgele erişim için[]rune(s)kullan.- Byte sınırlarını gözetmeden kesmek.
s[:10]gibi bir kesme Türkçe bir harfi ortadan bölebilir. Metni belirli sayıda karaktere kısaltırken rune'lar üzerinden çalış. - String'i byte byte ters çevirmek. Çok byte'lı karakterleri bozar; rune dilimi üzerinden ters çevir.
- Türkçe metinde
strings.ToUpper/ToLowerkullanmak. "istanbul" "ISTANBUL" olur. Türkçe metindeToUpperSpecial(unicode.TurkishCase, s)kullan; dilden bağımsız tanımlayıcılarda standart fonksiyonlarda kal. - Döngüde
+=ile string biriktirmek. Toplam maliyet O(n²) olur;strings.Builderveyastrings.Joinkullan. - Sayıyı
string(n)ile metne çevirmek. Bu bir kod noktası dönüşümüdür;strconv.Itoa(n)kullan. strconvhatalarını yok saymak.n, _ := strconv.Atoi(girdi)geçersiz girdide sessizce 0 üretir ve hata çok sonra, alakasız bir yerde ortaya çıkar. Hatayı her zaman kontrol et.
Alıştırmalar
Bir cümlenin palindrom olup olmadığını (tersten okunuşu aynı mı) döndüren isPalindrome(s string) bool fonksiyonunu yaz. Büyük/küçük harf farkını Türkçe kurallarla yok say; harf ve rakam dışındaki karakterleri (boşluk, noktalama) atla. "Ey Edip, Adana'da pide ye." ve "Işık kışı" için true, "Merhaba" için false dönmeli.
İpucu
Önce metni strings.ToLowerSpecial(unicode.TurkishCase, s) ile küçült. Sonra range ile gezip unicode.IsLetter veya unicode.IsDigit olan rune'ları bir []rune dilimine topla. Son olarak iki uçtan ortaya doğru karşılaştır.
Çözümü göster
package main
import (
"fmt"
"strings"
"unicode"
)
func isPalindrome(s string) bool {
var letters []rune
for _, r := range strings.ToLowerSpecial(unicode.TurkishCase, s) {
if unicode.IsLetter(r) || unicode.IsDigit(r) {
letters = append(letters, r)
}
}
for i, j := 0, len(letters)-1; i < j; i, j = i+1, j-1 {
if letters[i] != letters[j] {
return false
}
}
return true
}
func main() {
tests := []string{"Ey Edip, Adana'da pide ye.", "Kayak", "Işık kışı", "Merhaba", ""}
for _, t := range tests {
fmt.Printf("%-28q %t\n", t, isPalindrome(t))
}
// Türkçe kurallar olmadan küçültme:
fmt.Println(strings.ToLower("Işık kışı"))
}"Ey Edip, Adana'da pide ye." true "Kayak" true "Işık kışı" true "Merhaba" false "" true işık kışı
"Işık kışı" örneği Türkçe dönüşümün neden gerekli olduğunu gösteriyor. Standart ToLower baştaki I harfini i yapar ve harf dizisi "işıkkışı" olur. Bunun tersi "ışıkkışi"dir ve palindrom kontrolü yanlışlıkla false döner. TurkishCase ile harf dizisi "ışıkkışı" olur ve doğru sonuç çıkar. Karşılaştırmayı byte'lar yerine rune'lar üzerinde yaptığımız için iki byte'lık harfler de sorun çıkarmaz. Zaman karmaşıklığı O(n), rune dilimi için ek alan O(n).
Bir blog yazısının başlığından URL'de kullanılabilecek bir kısa ad üreten slugify(title string) string fonksiyonunu yaz. Kurallar: Türkçe kurallarla küçük harfe çevir; ç ğ ı ö ş ü harflerini c g i o s u yap; yalnızca a-z ve 0-9 karakterlerini koru; diğer karakter gruplarını tek bir - ile değiştir; baştaki ve sondaki tireleri at. Örnek: "Güzel Şehir İstanbul'a Hoş Geldiniz!" → guzel-sehir-istanbul-a-hos-geldiniz.
İpucu
Harf eşlemesi için strings.NewReplacer("ç", "c", "ğ", "g", ...) ile bir değiştirici oluşturabilirsin. Sonucu strings.Builder ile rune rune kur. Art arda gelen geçersiz karakterlerden yalnızca bir tire üretmek için son yazdığın karakterin tire olup olmadığını bir bool değişkende tut.
Çözümü göster
package main
import (
"fmt"
"strings"
"unicode"
)
var trToASCII = strings.NewReplacer(
"ç", "c", "ğ", "g", "ı", "i", "ö", "o", "ş", "s", "ü", "u",
)
func slugify(title string) string {
lower := strings.ToLowerSpecial(unicode.TurkishCase, title)
lower = trToASCII.Replace(lower)
var b strings.Builder
lastDash := false
for _, r := range lower {
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') {
b.WriteRune(r)
lastDash = false
} else if !lastDash && b.Len() > 0 {
b.WriteByte('-')
lastDash = true
}
}
return strings.TrimSuffix(b.String(), "-")
}
func main() {
fmt.Println(slugify("Güzel Şehir İstanbul'a Hoş Geldiniz!"))
fmt.Println(slugify(" Go ile Veri Yapıları: Bölüm 2 "))
fmt.Println(slugify("ÇAĞDAŞ IŞIK ÖĞÜT"))
}guzel-sehir-istanbul-a-hos-geldiniz go-ile-veri-yapilari-bolum-2 cagdas-isik-ogut
Sıra önemlidir: Önce Türkçe kurallarla küçültüyoruz, sonra harfleri ASCII'ye çeviriyoruz. Ters sırada yapsaydık "IŞIK" önce "ISIK"e, sonra standart kurallarla "isik"e dönerdi. Bu örnekte sonuç tesadüfen aynı olsa da "İ" ve "I" ayrımı başka girdilerde kaybolurdu. b.Len() > 0 koşulu baştaki tireleri, TrimSuffix ise sondaki tek tireyi engeller. strings.NewReplacer birden çok değiştirmeyi tek geçişte yapar ve eşzamanlı kullanım için güvenlidir, bu yüzden paket düzeyinde bir kez oluşturulabilir. Girdi uzunluğu n için zaman O(n).
Noktalı virgülle ayrılmış tutarlardan oluşan bir satırı işleyen parseAmounts(line string) (float64, []string) fonksiyonunu yaz. Her alanın başındaki ve sonundaki boşlukları at, boş alanları atla. Ondalık ayırıcı olarak hem nokta hem virgül kabul et. Geçerli tutarların toplamını ve ayrıştırılamayan alanları (strconv.Quote ile tırnaklı biçimde) döndür. "12; 7,5 ; abc; 40;; 3.25; 1,2,3" için toplam 62.75, hatalı alanlar "abc" ve "1,2,3" olmalı.
İpucu
strings.Split(line, ";") ile alanlara ayır, her alana strings.TrimSpace uygula. Virgülü strings.Replace(field, ",", ".", 1) ile yalnızca bir kez noktaya çevir; birden fazla virgül içeren alan zaten ParseFloat'tan hata alacaktır. Toplamı iki basamakla yazdırmak için strconv.FormatFloat kullan.
Çözümü göster
package main
import (
"fmt"
"strconv"
"strings"
)
func parseAmounts(line string) (float64, []string) {
var sum float64
var bad []string
for _, field := range strings.Split(line, ";") {
field = strings.TrimSpace(field)
if field == "" {
continue
}
normalized := strings.Replace(field, ",", ".", 1)
v, err := strconv.ParseFloat(normalized, 64)
if err != nil {
bad = append(bad, strconv.Quote(field))
continue
}
sum += v
}
return sum, bad
}
func main() {
sum, bad := parseAmounts("12; 7,5 ; abc; 40;; 3.25; 1,2,3")
fmt.Println("toplam:", strconv.FormatFloat(sum, 'f', 2, 64))
fmt.Println("hatalı:", strings.Join(bad, ", "))
sum, bad = parseAmounts(" ")
fmt.Println("toplam:", sum, "hatalı sayısı:", len(bad))
}toplam: 62.75 hatalı: "abc", "1,2,3" toplam: 0 hatalı sayısı: 0
Her alan için ParseFloat hatası kontrol ediliyor ve hatalı alan toplama katılmadan listeye ekleniyor. Böylece tek bir bozuk değer bütün satırı geçersiz kılmıyor ve kullanıcıya hangi alanların sorunlu olduğu gösterilebiliyor. "1,2,3" alanında yalnızca ilk virgül noktaya çevrildiği için "1.2,3" oluşur ve ParseFloat bunu reddeder. Hatalı alanları strconv.Quote ile yazdırmak, boşluk veya görünmeyen karakter içeren girdileri fark etmeyi kolaylaştırır. Satır uzunluğu n için zaman O(n).
Art arda tekrar eden karakterleri "karakter + tekrar sayısı" biçiminde kodlayan encode(s string) string ve bunu geri çözen decode(s string) (string, error) fonksiyonlarını yaz. Örneğin "aaaççğğğğğğğğğğğs" → "a3ç2ğ11s1". Girdide rakam olmadığını varsayabilirsin. Çözümün Türkçe karakterlerde doğru çalışmalı, iki basamaklı sayıları desteklemeli ve decode bozuk girdide (örneğin "ş3ğ") açıklayıcı bir hata döndürmeli.
İpucu
encode için string'i []rune'a çevir ve her gruptaki aynı rune'ları sayan bir iç döngü kullan. Sonucu strings.Builder ile kur, sayıyı strconv.Itoa ile yaz. decode için de rune dilimi üzerinde ilerle: Önce bir harf oku, ardından unicode.IsDigit olduğu sürece rakamları topla ve strconv.Atoi ile sayıya çevir. Rakam dizisi boşsa Atoi zaten hata döndürür.
Çözümü göster
package main
import (
"fmt"
"strconv"
"strings"
"unicode"
)
func encode(s string) string {
runes := []rune(s)
var b strings.Builder
for i := 0; i < len(runes); {
j := i
for j < len(runes) && runes[j] == runes[i] {
j++
}
b.WriteRune(runes[i])
b.WriteString(strconv.Itoa(j - i))
i = j
}
return b.String()
}
func decode(s string) (string, error) {
runes := []rune(s)
var b strings.Builder
for i := 0; i < len(runes); {
ch := runes[i]
if unicode.IsDigit(ch) {
return "", fmt.Errorf("%d. konumda harf bekleniyordu", i)
}
j := i + 1
for j < len(runes) && unicode.IsDigit(runes[j]) {
j++
}
count, err := strconv.Atoi(string(runes[i+1 : j]))
if err != nil {
return "", fmt.Errorf("%q için tekrar sayısı okunamadı: %w", ch, err)
}
b.WriteString(strings.Repeat(string(ch), count))
i = j
}
return b.String(), nil
}
func main() {
for _, s := range []string{"aaaççğğğğğğğğğğğs", "Gökçe", ""} {
enc := encode(s)
dec, err := decode(enc)
fmt.Printf("%q -> %q -> eşit: %t, hata: %v\n", s, enc, dec == s, err)
}
_, err := decode("ş3ğ")
fmt.Println(err)
_, err = decode("3a")
fmt.Println(err)
}"aaaççğğğğğğğğğğğs" -> "a3ç2ğ11s1" -> eşit: true, hata: <nil> "Gökçe" -> "G1ö1k1ç1e1" -> eşit: true, hata: <nil> "" -> "" -> eşit: true, hata: <nil> 'ğ' için tekrar sayısı okunamadı: strconv.Atoi: parsing "": invalid syntax 0. konumda harf bekleniyordu
encode her grubu bir kez gezer: j aynı rune devam ettiği sürece ilerler, grup bitince rune ve j - i uzunluğu yazılır ve i doğrudan j'ye atlar. İç ve dış döngü birlikte her rune'u yalnızca bir kez ziyaret ettiği için zaman O(n)'dir. Byte'lar üzerinde çalışsaydık ç harfinin iki byte'ı ayrı gruplar gibi görünür ve çözme bozuk UTF-8 üretirdi. decode tarafında runes[i+1 : j] rakam dizisidir; boşsa strconv.Atoi("") hata verir ve bu hatayı %w ile sarmalayarak neden başarısız olduğumuzu da mesajda koruyoruz. Çıktı uzunluğu m için decode O(n + m) sürer.
Kısa sınav
len("ışık") kaç döndürür?
s := "çay"; fmt.Println(s[0]) ne yazdırır?
Türkçe bir metinde "istanbul" kelimesini "İSTANBUL" yapan ifade hangisidir?
for i := range "aşk" { fmt.Print(i, " ") } ne yazdırır?
Bir döngüde n kez s += "x" çalıştırmanın toplam zaman karmaşıklığı nedir?
n, err := strconv.Atoi("42a") sonrasında hangisi doğrudur?
Özet
- String, değiştirilemeyen bir byte dizisidir.
len(s)byte sayısını,s[i]bir byte'ı verir;s[i:j]byte'ları kopyalamadan alt string oluşturur. - İçeriği değiştirmek için
[]byteveya[]rune'a çevirip sonucu yeni bir string'e dönüştürürsün; iki dönüşüm de kopyalama yapar. - Go string literalleri UTF-8'dir: ASCII 1 byte,
ç ğ ı ö ş üve büyükleri 2 byte,₺3 byte tutar.rune(int32) bir Unicode kod noktasıdır. - Karakter sayısı için
utf8.RuneCountInString, sırayla gezmek içinfor i, r := range s(i byte indeksidir), rastgele karakter erişimi için[]rune(s)kullan. String'i ters çevirmek gibi işlemleri rune'lar üzerinde yap. - Türkçe metinde
strings.ToUpperSpecial(unicode.TurkishCase, s)veToLowerSpecialkullan;strings.ToUpper("istanbul")"ISTANBUL" üretir. Tam dil desteği (başlık biçimi, alfabetik sıralama) içingolang.org/x/textmodülü vardır. - Döngüde string biriktirmek için
+=yerinestrings.Builder, dilimleri birleştirmek içinstrings.Joinkullan. stringspaketi arama (Contains,Index), bölme (Split,Fields,Cut), değiştirme (Replace) ve kırpma (TrimSpace) fonksiyonları sunar; konumlar byte indeksidir.strconv.Atoi,ParseFloatve benzerleri hata döndürür; hatayı her zaman kontrol et. Türkçe ondalık virgülParseFloattarafından kabul edilmez.