Go'da String'lerle Çalışmak: strings, strconv, fmt Verb'leri, byte/rune Farkı ve Türkçe Karakterler
Go'da string değiştirilemeyen bir UTF-8 byte dizisidir ve strings package'ı dile bakmaz: strings.EqualFold "istanbul" ile "İSTANBUL"u eşit saymaz, Türkçe kurallar için unicode.TurkishCase gerekir. ISBN normalize edip doğrulamak, strconv hata tipleri, Türkçe biçimli fiyat okumak, fmt verb'leri ve go vet, strings.Builder ölçümü ve RE2 tabanlı regexp.
Sürüm notu: Bu yazıdaki kodlar Go 1.27.4 ile derlenip çalıştırılmıştır; Bookstore module'ünün go satırı 1.26.0'dır ve kod Go 1.26.11 ile de derlenip testlerden geçer. strings.CutLast Go 1.27 ile geldiği için onu gösteren lab //go:build go1.27 ile ayrılmıştır. Go 1.27'de unicode package'ı Unicode 17'ye güncellendi.
Önce Tahmin Et
Sitedeki serilerin ortak örnek uygulaması Bookstore'da (bir online kitapçı) kargo ekibi teslimat şehrini büyük harfle, "İSTANBUL" diye kaydediyor. Müşteri hizmetlerinden Gül arama kutusuna "istanbul" yazıyor ve kod iki string'i strings.EqualFold ile, yani büyük/küçük harfe bakmadan karşılaştırıyor. Eşleşir mi? Bir de "ISPARTA"'yı strings.ToLower'dan geçirince ne çıkar?
func main() {
tr := unicode.TurkishCase
fmt.Println(strings.ToUpper("istanbul"), strings.ToUpperSpecial(tr, "istanbul"))
fmt.Println(strings.ToLower("ISPARTA"), strings.ToLowerSpecial(tr, "ISPARTA"))
fmt.Println(strings.ToLower("İZMİR"), strings.ToLowerSpecial(tr, "İZMİR"))
fmt.Println(strings.EqualFold("Dune", "DUNE"))
fmt.Println(strings.EqualFold("istanbul", "İSTANBUL"), strings.EqualFold("ısparta", "ISPARTA"))
}
$ go run ./labs/15-turkish
ISTANBUL İSTANBUL
isparta ısparta
izmir izmir
true
false false
Eşleşmiyor. EqualFold "Dune" ile "DUNE"'yi eşit buluyor ama "istanbul" ile "İSTANBUL"u bulmuyor; ToLower da "ISPARTA"'yı "isparta" yapıyor. Go'nun strings fonksiyonları dile bakmıyor: Unicode'un herkes için tanımladığı eşlemeyi kullanıyorlar ve o eşlemede I'nın küçüğü i, i'nin büyüğü I. C#'ta ToUpper() varsayılan olarak CurrentCulture'a bakar; Go'da böyle bir "geçerli kültür" yok. Türkçe kurallar gerekiyorsa bunu açıkça unicode.TurkishCase ile söylüyorsun. Bu yazıda Go'da string'lerle günlük işleri görüyoruz: byte ve rune farkı, strings, strconv, fmt verb'leri, string birleştirmenin maliyeti ve kısaca regexp. Bookstore tarafında ISBN (International Standard Book Number; kitaplara verilen 13 haneli uluslararası numara) normalize edip doğrulayan bir fonksiyon ve Türkçe biçimli bir fiyat listesini okuyan bir lab yazıyoruz.
string, byte ve rune: Kısa Tekrar
03'te gördüğümüz temel kural şu: Go'da string değiştirilemeyen bir byte dizisi, kaynak kod ve string literal'lar UTF (Unicode Transformation Format; Unicode karakterlerini byte'lara çeviren kodlama ailesi) ailesinden UTF-8 ile yazılır ve UTF-8 bir karakteri 1–4 byte ile kodlar. len byte sayar, harf saymaz; s[i] bir byte döndürür; for range ise string'i rune rune dolaşır. C#'ta string UTF-16 birimlerinden oluşuyor ve s.Length UTF-16 birimi sayıyor; Go'da birim byte. Bu farkın pratikteki en sık sonucu kesme işlemi:
// Lab 15: byte'lar, rune'lar ve len'in gerçekte neyi saydığı.
package main
import (
"fmt"
"unicode/utf8"
)
// firstN, s'nin en fazla n karakterini bir rune'u ortadan bölmeden döndürür.
func firstN(s string, n int) string {
i := 0
for pos := range s { // pos, her rune'un başladığı byte index'i
if i == n {
return s[:pos]
}
i++
}
return s
}
func main() {
title := "Çalıkuşu"
fmt.Println(len(title), utf8.RuneCountInString(title))
fmt.Printf("%q %q\n", title[:5], firstN(title, 5))
for pos, r := range "Dün" {
fmt.Printf("%d:%c(%U) ", pos, r, r)
}
fmt.Println()
b := []byte("Dune")
b[0] = 'J' // []byte değişebilir; geldiği string değişemez
fmt.Println(string(b))
broken := "Emma\xff" // yanlış kodlanmış bir dosyadan gelen geçersiz UTF-8
fmt.Println(utf8.ValidString(broken), []rune(broken)[4] == utf8.RuneError)
}
$ go run ./labs/15-runes
11 8
"Çal\xc4" "Çalık"
0:D(U+0044) 1:ü(U+00FC) 3:n(U+006E)
June
false true
title[:5] beş byte aldı ve "ı" harfinin iki byte'ından birini ortadan kesti; %q bu yüzden sonuna \xc4 yazdı. Böyle bir string'i JSON'a yazarsan bozuk bir karakter gönderirsin. firstN, range'in verdiği byte index'lerini kullanarak beşinci rune'un başladığı yerde kesiyor; []rune(s) dönüşümü de aynı işi yapar ama bütün string'i kopyalar. Son satır geçersiz UTF-8'in nasıl göründüğünü gösteriyor: "\xff" bir karakter değil, range ve []rune onu utf8.RuneError (U+FFFD) olarak veriyor. Dışarıdan gelen bir dosyanın kodlaması bozuksa utf8.ValidString bunu baştan söyler. 10'da json/v2'nin geçersiz UTF-8'i reddettiğini görmüştük; v1 ise onu sessizce U+FFFD'ye çeviriyor.
strings Package'ı: Cut, Map, Fields ve ISBN
Kullanıcılar ISBN'i her biçimde yazıyor: "978-0-441-17271-9", "ISBN 978 0 441 17271 9", "isbn: 9780441172719". Bookstore'un katalogunda ise tek biçim var: tiresiz 13 hane. internal/catalog'a gelen yeni fonksiyon girdiyi normalize ediyor ve son hanenin, yani kontrol hanesinin doğru olduğunu kontrol ediyor:
// NormalizeISBN "ISBN 978-0-441-17271-9"u "9780441172719"a çevirir ve
// kontrol hanesini doğrular. Tire ve boşluklar atılır, başka hiçbir şey atılmaz.
func NormalizeISBN(s string) (string, error) {
s = strings.TrimSpace(s)
if len(s) >= 4 && strings.EqualFold(s[:4], "isbn") {
s = strings.TrimLeft(s[4:], " :-")
s = strings.TrimLeft(strings.TrimPrefix(s, "13"), " :") // "ISBN-13: ..."
}
isbn := strings.Map(func(r rune) rune {
if r == '-' || r == ' ' {
return -1 // rune'u at
}
return r
}, s)
if len(isbn) != 13 || !digits(isbn) {
return "", fmt.Errorf("%q: %w", s, ErrInvalidISBN)
}
if !strings.HasPrefix(isbn, "978") && !strings.HasPrefix(isbn, "979") {
return "", fmt.Errorf("%q: prefix must be 978 or 979: %w", s, ErrInvalidISBN)
}
if want := checkDigit(isbn[:12]); isbn[12] != want {
return "", fmt.Errorf("%q: check digit is %c, want %c: %w", s, isbn[12], want, ErrInvalidISBN)
}
return isbn, nil
}
// checkDigit 13. haneyi hesaplar: ağırlıklar 1 ve 3 sırayla.
func checkDigit(first12 string) byte {
sum := 0
for i := range len(first12) {
d := int(first12[i] - '0') // rune değil byte: rakamlar ASCII
if i%2 == 1 {
d *= 3
}
sum += d
}
return byte('0' + (10-sum%10)%10)
}
Kullandığımız fonksiyonların çoğunun C#'ta bir karşılığı var: TrimSpace → Trim(), HasPrefix → StartsWith, EqualFold → string.Equals(a, b, StringComparison.OrdinalIgnoreCase). strings.Map her rune için bir fonksiyon çağırıyor ve fonksiyon negatif değer dönerse o rune'u atıyor; tire ve boşluğu silmenin en kısa yolu bu. checkDigit içinde first12[i] ile byte okumak bilinçli bir seçim: girdinin sadece ASCII (İngilizce harfler, rakamlar ve temel işaretlerden oluşan 128 karakterlik tablo) rakamlardan oluştuğunu bir satır önce digits ile kontrol ettik. unicode.IsDigit kullansaydık Arapça rakamlar da geçerdi; testteki "٩٧٨..." satırı bunun için var. GET /api/books/{isbn} artık her iki biçimi kabul ediyor:
$ curl -i http://127.0.0.1:52715/api/books/978-0-441-17271-9
HTTP/1.1 200 OK
Content-Type: application/json
{"isbn":"9780441172719","title":"Dune","author":"Frank Herbert","price":34990,"stock":12}
$ curl -i http://127.0.0.1:52715/api/books/9780441172718
HTTP/1.1 400 Bad Request
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
"9780441172718": check digit is 8, want 9: invalid ISBN-13
Daha önce bu istek 404 dönüyordu; şimdi istemci, numarayı yanlış yazdığını kitabın olmadığından ayırabiliyor. 12'deki handler testine iki satır ekledim: tireli ISBN için 200, kontrol hanesi yanlış olan 9780000000000 için 400. 404 satırı da geçerli ama katalogda olmayan 9780000000002'ye taşındı. strings.Cut'ı 11'deki ParseMoney'de kullanmıştık; Go 1.27 bir de CutLast getirdi. İkisi de üç değer dönüyor ve Index + dilimleme ikilisinin yerini alıyor:
func main() {
name := "orders.2026-11-14.csv"
before, after, found := strings.Cut(name, ".")
fmt.Printf("Cut: %q %q %v\n", before, after, found)
base, ext, found := strings.CutLast(name, ".")
fmt.Printf("CutLast: %q %q %v\n", base, ext, found)
}
$ go run ./labs/15-cutlast
Cut: "orders" "2026-11-14.csv" true
CutLast: "orders.2026-11-14" "csv" true
Ayırma için iki fonksiyon daha bilmek yeterli: strings.Split ayırıcıya göre böler ve boş string için bile tek elemanlı bir slice ([""]) döner; strings.Fields ardışık boşlukları tek ayırıcı sayar ve boş string için boş slice döner. Go 1.24'ten beri SplitSeq, FieldsSeq ve Lines aynı işi slice oluşturmadan, iterator olarak yapıyor.
Türkçe Karakterler: TurkishCase ve EqualFold
Önce Tahmin Et'teki çıktıya geri dönelim. strings.ToUpperSpecial(unicode.TurkishCase, s) ve ToLowerSpecial Türkçe ve Azerice'nin i/ı kurallarını uyguluyor. İlginç bir ayrıntı: Go'da strings.ToLower("İZMİR") "izmir" veriyor, çünkü Unicode'un basit eşlemesi İ'yi doğrudan i'ye çeviriyor. Sorun ters yönde: I her zaman i oluyor, i her zaman I. Türkçe adlar ve şehirlerle çalışırken iki kural işini görür:
- Ekranda göstereceğin ya da Türkçe metinde arayacağın değerleri
unicode.TurkishCaseile dönüştür; büyük/küçük harf duyarsız Türkçe karşılaştırma için iki tarafıToLowerSpecial(unicode.TurkishCase, ...)'tan geçirip==ile karşılaştır. - Kod, ISBN, sipariş numarası, HTTP header'ı gibi makine değerleri için
EqualFolddoğru araç; bunlarda Türkçe kural istemezsin.
EqualFold Unicode'un "simple case folding" tablosunu kullanıyor ve o tabloda İ ile i, ı ile I birbirine katlanmıyor; iki false bu yüzden. Daha kapsamlı ihtiyaçlar için (dile göre büyük harf, Türkçe alfabe sırasıyla sıralama) golang.org/x/text module'ündeki cases ve collate package'ları var; sıralamaya 17'de döneceğiz.
strconv: Sayıya Çevirmek ve Hata Tipleri
C#'taki int.Parse/int.TryParse ikilisinin Go'daki karşılığı tek fonksiyon: strconv.Atoi değeri ve bir error dönüyor. Hata bir *strconv.NumError; içindeki sebebi errors.Is ile ayırt edebilirsin:
for _, in := range []string{"42", "12a", "99999999999999999999"} {
n, err := strconv.Atoi(in)
fmt.Println(n, err, errors.Is(err, strconv.ErrSyntax), errors.Is(err, strconv.ErrRange))
}
f, err := strconv.ParseFloat("349,90", 64)
fmt.Println(f, err)
fmt.Println(strconv.Quote("Dune\t\"Arrakis\""), strconv.FormatInt(34990, 10))
$ go run ./labs/15-strconv
42 <nil> false false
0 strconv.Atoi: parsing "12a": invalid syntax true false
9223372036854775807 strconv.Atoi: parsing "99999999999999999999": value out of range false true
0 strconv.ParseFloat: parsing "349,90": invalid syntax
"Dune\t\"Arrakis\"" 34990
...
İki ayrıntıya dikkat. Değer aralık dışındaysa Atoi 0 değil, tipin alabileceği en büyük değeri dönüyor; hatayı kontrol etmeyen kod 9.223.372.036.854.775.807 adetlik bir siparişle devam eder. ParseFloat ise C#'taki gibi bir CultureInfo almıyor: ondalık ayırıcı her zaman nokta. Fiyatı zaten 10'dan beri float64 ile taşımıyoruz ama Türkçe Excel'den gelen bir dosyada virgül kaçınılmaz. Tedarikçinin gönderdiği CSV (Comma-Separated Values; alanları virgül ya da noktalı virgülle ayrılmış düz metin) dosyasının birkaç satırı:
// priceList, tedarikçinin Türkçe bir Excel sayfasından aldığı çıktı.
const priceList = `isbn;title;price
// parseTRPrice "1.249,50 TL"yi okur: binlik ayırıcı nokta, ondalık ayırıcı virgül.
func parseTRPrice(s string) (catalog.Money, error) {
s = strings.TrimSpace(strings.TrimSuffix(strings.TrimSpace(s), "TL"))
s = strings.ReplaceAll(s, ".", "")
s = strings.Replace(s, ",", ".", 1)
return catalog.ParseMoney(s)
}
...
line 2: 9780441172719 "Dune" 349.90 TRY
line 3: 9780547928227 "The Hobbit" 1249.50 TRY
line 4: 9780141439549 "Middlemarch" 310.00 TRY
line 5: "abc": invalid money amount
parseTRPrice önce " TL" ekini ve binlik ayırıcı noktaları atıyor, virgülü noktaya çeviriyor ve sonucu float64'e hiç uğramadan catalog.ParseMoney'ye veriyor. Satırları strings.Split ile ayırmak bu küçük örnek için yeterli, ama tırnak içinde noktalı virgül taşıyan bir alan geldiği anda bozulur; gerçek CSV için encoding/csv'yi 18'de kullanacağız. Ters yönde strconv.FormatInt, Quote ve slice'a yazan AppendInt var; Quote log'a kontrol karakteri içeren bir değer yazarken işe yarıyor.
fmt Verb'leri ve Stringer
C#'ta string interpolation ($"{book.Title} has {book.Stock}") kullanırsın; Go'da interpolation yok, fmt.Sprintf ve verb'ler var. En çok kullanılanlar:
func main() {
b := catalog.Book{ISBN: "9780441172719", Title: "Dune", Author: "Frank Herbert", Price: 34990, Stock: 12}
fmt.Printf("%v\n", b)
fmt.Printf("%+v\n", b)
fmt.Printf("%#v\n", b)
fmt.Printf("%T %T\n", b, b.Price)
// Money'nin String method'u var: %v ve %s onu kullanır, %d kullanmaz.
fmt.Printf("%v | %s | %d\n", b.Price, b.Price, b.Price)
fmt.Printf("%q %x % x\n", b.Title, b.Title, "Dü")
fmt.Printf("[%-12s][%15s][%6.2f][%05d]\n", b.Title, b.Author, 349.9, 42)
fmt.Printf("%v %d\n", orders.StatusPaid, orders.StatusPaid)
err := fmt.Errorf("place order: %w", orders.ErrOutOfStock)
fmt.Println(err, errors.Is(err, orders.ErrOutOfStock))
s := fmt.Sprintf("%s has %d in stock", b.Title, b.Stock)
fmt.Println(s)
}
$ go run ./labs/15-fmt
{9780441172719 Dune Frank Herbert 349.90 TRY 12}
{ISBN:9780441172719 Title:Dune Author:Frank Herbert Price:349.90 TRY Stock:12}
catalog.Book{ISBN:"9780441172719", Title:"Dune", Author:"Frank Herbert", Price:34990, Stock:12}
catalog.Book catalog.Money
349.90 TRY | 349.90 TRY | 34990
"Dune" 44756e65 44 c3 bc
[Dune ][ Frank Herbert][349.90][00042]
Paid 1
place order: out of stock true
Dune has 12 in stock
%v varsayılan biçim, %+v struct'ta field adlarını ekliyor, %#v değeri Go kodu olarak yazıyor ve debug için en çok işe yarayanı bu. 04'teki Stringer interface'ini karşılayan tipler (Money, Status) %v ve %s'de kendi String method'larıyla yazılıyor; %#v ve %d ise ham değeri gösteriyor. %q tırnaklı ve escape'li yazıyor, % x byte'ları aralarına boşluk koyarak hex yazıyor ve "ü"nün iki byte olduğunu (c3 bc) burada da görüyorsun. Genişlik bir minimum: %-12s sola dayayıp 12'ye tamamlıyor. %w'yu 05'te gördük; sadece fmt.Errorf'ta anlamlı. Verb ile argüman uyuşmazsa program derleniyor ve çalışıyor, hatayı çıktının içine yazıyor:
package main
import "fmt"
func main() {
title, stock := "Dune", 12
fmt.Printf("%d has %d in stock\n", title, stock) // string için %d
fmt.Printf("%s has %d in stock\n", title) // bir argüman eksik
}
$ go run .
%!d(string=Dune) has 12 in stock
Dune has %!d(MISSING) in stock
$ go vet .
main.go:7:14: fmt.Printf format %d has arg title of wrong type string
main.go:8:21: fmt.Printf format %d reads arg #2, but call has 1 arg
go vet'in printf kontrolü bu hataları build'den önce yakalıyor; go test da bu kontrolü kendiliğinden çalıştırıyor. C#'ın derleyicinin kontrol ettiği interpolation'ına en yakın güvence bu.
strings.Builder ve + ile Birleştirmek
Go'da string değiştirilemediği için s += t her seferinde yeni bir string ayırıp o ana kadarki her şeyi kopyalıyor. C#'taki StringBuilder'ın karşılığı strings.Builder. 1.000 başlıktan bir liste oluşturan dört yöntemi ölçtüm:
func BenchmarkPlus(b *testing.B) {
for b.Loop() {
s := ""
for _, t := range titles {
s += t + "\n" // her seferinde yeni bir string ve o ana kadarki her şeyin kopyası
}
_ = s
}
}
func BenchmarkBuilderGrow(b *testing.B) {
for b.Loop() {
var sb strings.Builder
sb.Grow(len(titles) * (len("Middlemarch") + 1)) // tek allocation
for _, t := range titles {
sb.WriteString(t)
sb.WriteByte('\n')
}
_ = sb.String()
}
}
$ go test -run '^$' -bench . -benchmem -cpu 1 -count 3 ./labs/15-builder
BenchmarkPlus 2852 450433 ns/op 6366120 B/op 1000 allocs/op
BenchmarkPlus 2343 692214 ns/op 6366120 B/op 1000 allocs/op
BenchmarkPlus 1561 664994 ns/op 6366120 B/op 1000 allocs/op
BenchmarkBuilder 95614 12677 ns/op 46576 B/op 15 allocs/op
BenchmarkBuilder 123186 10284 ns/op 46576 B/op 15 allocs/op
BenchmarkBuilder 168128 7274 ns/op 46576 B/op 15 allocs/op
BenchmarkBuilderGrow 240349 5004 ns/op 12288 B/op 1 allocs/op
BenchmarkBuilderGrow 242506 5104 ns/op 12288 B/op 1 allocs/op
BenchmarkBuilderGrow 229485 5384 ns/op 12288 B/op 1 allocs/op
BenchmarkJoin 190495 6785 ns/op 12288 B/op 1 allocs/op
BenchmarkJoin 172795 6574 ns/op 12288 B/op 1 allocs/op
BenchmarkJoin 171823 6487 ns/op 12288 B/op 1 allocs/op
Süreler başka işlerin de çalıştığı bir makinede ölçüldü ve dalgalanıyor, ama allocation sütunu net: + her çağrıda 6,3 MB ayırıyor, Builder 46 KB, önceden Grow ile yer ayıran Builder ve strings.Join tek bir 12 KB'lık allocation yapıyor. İki ya da üç parçayı birleştirmek için + gayet iyi; derleyici tek ifadedeki birleştirmeleri zaten tek seferde yapıyor. Sorun döngüde büyütmek. Builder'ın bir kuralı var: kullanılmaya başlanmış bir Builder'ın kopyasına yazmak panic'e yol açıyor, bu yüzden fonksiyonlara pointer olarak geçir.
regexp: RE2 Kısaca
regexp package'ı RE2 (Google'ın geliştirdiği, eşleşme süresi girdinin uzunluğuyla doğru orantılı kalan regex motoru) syntax'ını kullanıyor. Bunun karşılığında .NET'in regex motorundaki bazı özellikler yok:
func main() {
fmt.Println(isbnLike.MatchString("9780441172719"), isbnLike.MatchString("9780441172718"))
price := regexp.MustCompile(`(\d+),(\d{2}) TL`)
fmt.Println(price.FindStringSubmatch("Dune 349,90 TL"))
//lint:ignore SA1000 the lab shows the error on purpose
_, err := regexp.Compile(`(?<=ORD-)\d+`) // lookbehind, .NET'te sorunsuz
fmt.Println(err)
}
$ go run ./labs/15-regexp
true true
[349,90 TL 349 90]
error parsing regexp: invalid named capture: `(?<=ORD-)\d+`
Lookbehind, lookahead ve backreference yok; Go (?<name>...) yazımını named group olarak okuduğu için hata mesajı da "invalid named capture" diyor. Karşılığında kötü yazılmış bir pattern'in kullanıcı girdisiyle CPU'yu kilitlemesi gibi bir risk de yok; .NET'te bunun için RegexOptions.NonBacktracking ya da timeout gerekiyor. Lab'daki //lint:ignore satırını silince staticcheck geçersiz pattern'i derleme aşamasında yakalıyor: error parsing regexp: invalid named capture: `(?<=ORD-)\d+` (SA1000). İlk satırdaki ikinci true da önemli: regex sadece biçime bakıyor, kontrol hanesi yanlış olan ISBN'i kabul ediyor. Basit biçim kontrollerinde strings fonksiyonları hem daha hızlı hem daha okunaklı; regex'i gerçekten pattern gereken yerlere bırak ve MustCompile ile package seviyesinde bir kere derle.
Üretimde Dikkat
Dışarıdan gelen metni sınırda normalize et. ISBN'i handler'da bir kere NormalizeISBN'den geçirince katalog, veritabanı ve log tek biçim görüyor; aynı şeyi müşteri e-postası (TrimSpace + ToLower; e-postada Türkçe kural istemezsin) ve kupon kodu için yap. Kesme ve uzunluk sınırlarını byte ile mi karakterle mi koyduğuna karar ver: veritabanındaki nvarchar(200) karakter sayıyor, len byte sayıyor.
Hata mesajına kullanıcı girdisini koyarken %q kullan. NormalizeISBN'in hataları girdiyi tırnak içinde ve escape'lenmiş yazıyor; böylece içinde satır sonu taşıyan bir değer log'da sahte bir satır oluşturamıyor. slog'un handler'ları bunu zaten yapıyor ama log.Printf ya da fmt ile yazılan log'larda fark ediyor.
Hot path'te (en sık çalışan kod yolu) fmt.Sprintf yerine strconv'u düşün. Sprintf argümanları interface'e çevirip reflection ile biçimlendiriyor; saniyede binlerce kez çağrılan bir yerde strconv.AppendInt ya da Builder fark yaratabilir. Ölçmeden değiştirme: 13'teki pprof profili hangi satırın önemli olduğunu gösteriyor.
Sık Düşülen Tuzaklar
s[i]'yi karakter sanmak. s[i] bir byte; Türkçe metni s[:n] ile kesmek bir harfi ortadan bölebilir. range ya da utf8 package'ını kullan.
Türkçe metinde strings.ToUpper/ToLower/EqualFold kullanmak. Bu fonksiyonlar dilden bağımsız; Türkçe için unicode.TurkishCase gerekiyor.
strconv hatasını yok saymak. n, _ := strconv.Atoi(s) hem bozuk girdide 0, hem aralık dışında en büyük değeri sessizce kabul ediyor.
ParseFloat'tan Türkçe sayı beklemek. Virgüllü sayıyı kabul etmiyor; dönüşümü kendin yap ve para için float64'e hiç uğrama.
Döngüde += ile string büyütmek. Küçük listelerde fark edilmez, büyük bir rapor ya da CSV üretiminde allocation'lar GC'yi (garbage collector; kullanılmayan belleği toplayan bileşen) meşgul eder.
Küçük Bir Deneme
★☆☆ labs/15-turkish'e sameTR(a, b string) bool fonksiyonunu ekle: iki string'i Türkçe kurallarla, büyük/küçük harfe bakmadan karşılaştırsın. "istanbul" ile "İSTANBUL" ve "ısparta" ile "ISPARTA" için true dönmeli.
★★☆ NormalizeISBN'i ISBN-10 kabul edecek şekilde genişlet: "0-441-17271-7" gibi bir numarayı 978 prefix'iyle ISBN-13'e çevir (ISBN-10'un kontrol hanesi 11'e göre hesaplanıyor ve "X" olabiliyor). Tablo testine en az dört satır ekle.
★★★ parseTRPrice'ı catalog package'ına taşı ve "1.249,50", "1249,5", "0,05", "1.2.3,00" ve "12,345" girdileri için bir tablo testi yaz. Hangilerinin hata vermesi gerektiğine karar ver; sonra 12'deki gibi bir fuzz testiyle Money.Decimal'ın Türkçe biçimli karşılığını yazıp round-trip'i kontrol et.
String'lerle işimiz bitmedi ama sıradaki konu, C# geliştiricisini Go'da en çok şaşırtan biçimlendirme kuralı: yyyy-MM-dd yerine 2006-01-02. Bir sonraki yazıda time package'ını, time zone'ları ve monotonic clock'u konuşuyor, 11'deki üç saatlik kayma hatasına da time zone tarafından bakıyoruz.