go
Go dersleri
Go/Temeller

String, Byte ve Rune

UTF-8, Türkçe karakterler, strings ve strconv paketleri.

Ders 7 / 2125 dkBaşlangıç
Bu derste öğreneceklerin
  • String'in değişmezliği ve byte dizisi olması
  • UTF-8 ve rune kavramı
  • len ile utf8.RuneCountInString farkı
  • range ile rune gezinme
  • Türkçe büyük/küçük harf dönüşümü: unicode.TurkishCase
  • strings.Builder ile verimli birleştirme
  • strings paketi: Split, Fields, Contains, Replace
  • strconv: Atoi, Itoa, ParseFloat, Quote

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.

main.go
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])
}
Çıktı
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:

main.go
package main

func main() {
	s := "kedi"
	s[0] = 'K' // derleme hatası
}
Çıktı
./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:

main.go
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)
}
Çıktı
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 string

UTF-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:

Kod noktası aralığıByte sayısıÖrnekler
U+0000 – U+007F1a, Z, 7, boşluk
U+0080 – U+07FF2ç, ğ, ı, ö, ş, ü, Ç, Ğ, İ, Ö, Ş, Ü
U+0800 – U+FFFF3,
U+10000 – U+10FFFF4𝄞 (sol anahtarı)

Aşağıdaki program her karakterin kod noktasını ve UTF-8 byte'larını yazdırıyor:

main.go
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)
	}
}
Çıktı
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): 4

len 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.

main.go
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))
	}
}
Çıktı
"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:

main.go
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
}
Çıktı
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:

main.go
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))
}
Çıktı
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           6

range 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:

main.go
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ı
}
Çıktı
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:

main.go
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))
	}
}
Çıktı
"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:

main.go
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'))
}
Çıktı
ISTANBUL
İSTANBUL
isparta
ısparta
ILIK ILIK
izmir izmir
I İ
i ı
Dönüşümstrings.ToUpper / ToLowerunicode.TurkishCase ile
i → büyükI (yanlış)İ
ı → büyükII
I → küçüki (yanlış)ı
İ → küçükii

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:

main.go
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)
}
Çıktı
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.

main.go
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")
}
Çıktı
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

main.go
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"))
}
Çıktı
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

main.go
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("", ",")))
}
Çıktı
["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

main.go
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)
}
Çıktı
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.

FonksiyonNe yapar?
Contains, ContainsAny, ContainsRuneAlt string, karakterlerden biri veya bir rune var mı?
HasPrefix, HasSuffixBelirli bir önek veya sonekle başlıyor/bitiyor mu?
Index, LastIndexİlk/son geçtiği byte indeksi, yoksa -1
Split, SplitNAyırıcıya göre dilime böler
Fields, FieldsFuncBoşluklara veya bir koşula göre böler, boş parçaları atar
JoinDilimi ayırıcıyla birleştirir
Cutİlk ayırıcıdan ikiye böler: önce, sonra, bulundu
Replace, ReplaceAllAlt string değiştirir
TrimSpace, Trim, TrimPrefix, TrimSuffixUçlardan kırpar
ToUpper, ToLower, ToUpperSpecial, ToLowerSpecialHarf dönüşümü
Repeat, Map, EqualFold, CountTekrar, rune bazında dönüşüm, harf duyarsız eşitlik, sayma

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

main.go
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))
}
Çıktı
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

main.go
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)
}
Çıktı
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.

FonksiyonÖrnekSonuç
strconv.Atoi(s)Atoi("42")42, nil
strconv.Itoa(n)Itoa(42)"42"
strconv.ParseFloat(s, 64)ParseFloat("2.5", 64)2.5, nil
strconv.FormatFloat(f, 'f', p, 64)FormatFloat(2.0/3, 'f', 2, 64)"0.67"
strconv.ParseInt(s, taban, bit)ParseInt("ff", 16, 64)255, nil
strconv.FormatInt(n, taban)FormatInt(5, 2)"101"
strconv.ParseBool(s)ParseBool("true")true, nil
strconv.Quote(s)Quote("a\tb")"\"a\\tb\""

Sık yapılan hatalar

  • Karakter sayısını len ile ölçmek. len byte 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çin utf8.RuneCountInString kullan.
  • s[i] ile i. harfe erişmeye çalışmak. İndeksleme byte döndürür. Harfleri sırayla işlemek için for _, 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/ToLower kullanmak. "istanbul" "ISTANBUL" olur. Türkçe metinde ToUpperSpecial(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.Builder veya strings.Join kullan.
  • Sayıyı string(n) ile metne çevirmek. Bu bir kod noktası dönüşümüdür; strconv.Itoa(n) kullan.
  • strconv hataları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

Alıştırma·Türkçe palindrom kontrolü
Kolay

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
main.go
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ışı"))
}
Çıktı
"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).

Alıştırma·URL dostu kısa ad (slug) üret
Orta

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
main.go
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"))
}
Çıktı
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).

Alıştırma·Tutar listesini ayrıştır
Orta

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
main.go
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))
}
Çıktı
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).

Alıştırma·Rune tabanlı sıkıştırma (RLE)
Zor

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
main.go
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)
}
Çıktı
"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

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 []byte veya []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çin for 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) ve ToLowerSpecial kullan; strings.ToUpper("istanbul") "ISTANBUL" üretir. Tam dil desteği (başlık biçimi, alfabetik sıralama) için golang.org/x/text modülü vardır.
  • Döngüde string biriktirmek için += yerine strings.Builder, dilimleri birleştirmek için strings.Join kullan.
  • strings paketi arama (Contains, Index), bölme (Split, Fields, Cut), değiştirme (Replace) ve kırpma (TrimSpace) fonksiyonları sunar; konumlar byte indeksidir.
  • strconv.Atoi, ParseFloat ve benzerleri hata döndürür; hatayı her zaman kontrol et. Türkçe ondalık virgül ParseFloat tarafından kabul edilmez.
Bu dersi bitirdin mi?
İlerlemen bu tarayıcıda saklanır.