Skip to content
← Go · মধ্যবর্তী · 15 মিনিট · 09 / 25 EN

Strings, Runes ও Encoding

Go-তে string আসলে UTF-8 byte slice — এটা জানা থাকলে international text, emoji আর binary data হ্যান্ডেল করার সময় একটা গোটা শ্রেণীর bug এড়ানো যায়।

stringsrunesUTF-8encodingbytestext processing

গল্পে বুঝি

আল-খোয়ারিজমি সাইনবোর্ড লেখে — দোকানের নাম, ঠিকানা, সব রঙ দিয়ে হাতে আঁকা। একদিন এক খদ্দের এসে বলল, “আমার দোকানের নাম ‘কৃষ্ণা’— কয়টা অক্ষর, সেই হিসাবে দাম নিও।” আল-খোয়ারিজমি চোখে দেখে গুনল: কৃ, ষ, ্‌, ণ, া — চোখে যতগুলো আলাদা টান আর টুকরো দেখা যায়, সব মিলিয়ে সে বলল “ছয়টা টান হবে, ছয় অক্ষরের দাম।” খদ্দের হেসে বলল, “আরে ভাই, নামটা তো মোটে তিন অক্ষর — ‘কৃ’, ‘ষ্ণ’, ‘া’। তুমি প্রতিটা অক্ষরের ভেতরের রঙের টানগুলো আলাদা করে গুনছ!”

আসল কথাটা এখানেই। চোখে দেখা একটা গোটা যুক্তাক্ষর, যেমন ‘ষ্ণ’, আল-খোয়ারিজমির তুলি একটা টানে আঁকে না — ষ, হসন্ত, ণ, এমন কয়েকটা আলাদা টানে বসাতে হয়। আল-খোয়ারিজমি যদি তুলির টান গোনে, সংখ্যা বেশি আসে; আর যদি চোখে-দেখা পুরো অক্ষর গোনে, সংখ্যা ঠিক আসে। একই নাম, কিন্তু “কী গুনছ” তার উপর দুই রকম উত্তর।

এটাই string বনাম rune-এর গল্প। চোখে-দেখা একটা গোটা অক্ষর হলো একটা rune (একটা Unicode code point), আর সেটা আঁকতে যে আলাদা তুলির টান লাগে সেগুলো হলো byte। Go-তে string হলো byte-এর sequence — UTF-8 encoding-এ ইংরেজি ASCII অক্ষর এক byte-এ ধরে, কিন্তু বাংলা অক্ষর বা emoji ধরতে ২-৪টা byte লাগে। তাই আল-খোয়ারিজমির মতো byte গুনলে (len(s)) সংখ্যা বেশি আসে, character গুনতে হলে rune গুনতে হয় (utf8.RuneCountInString)। বাস্তবে এটাই ভুলের জায়গা: username-এর length চেক, বাংলা বা emoji-সহ কমেন্টের অক্ষর গোনা, বা tweet-এর character limit — byte দিয়ে গুনলে বাংলা “কৃষ্ণা” বা ”😊” থাকা লেখা ভুল লম্বা দেখাবে, rune দিয়ে গুনলেই ইউজার চোখে যা দেখে তার সাথে মিলবে।

String হলো Byte Slice

Go-তে একটা string হলো read-only byte slice। character নয়, rune নয় — byte।

s := "Hello"
fmt.Println(len(s))    // 5 (bytes, not characters)
fmt.Println(s[0])      // 72 (byte value of 'H')
fmt.Println(string(s[0]))  // "H"

// UTF-8 multi-byte characters
s = "Hello 🌍"
fmt.Println(len(s))    // 10 (NOT 7!) — 🌍 is 4 bytes in UTF-8

বাস্তব জীবনের উপমা

Go-এর একটা string অনেকটা filmstrip-এর মতো। প্রতিটা frame (byte) হলো একেকটা টুকরো। সাধারণ ASCII character একটা করে frame ব্যবহার করে। কিন্তু জটিল character (চাইনিজ, আরবি, emoji) ২-৪টা frame ব্যবহার করে। len() frame গোনে, ছবি নয়। ছবি গুনতে হলে filmstrip টা decode করতে হবে।

Runes: Unicode Code Point

rune হলো Unicode code point-এর Go-এর নাম — একটা int32 যেটা একটা single character-কে প্রকাশ করে:

s := "Hello 🌍"

// Iterating by BYTES — wrong for multi-byte characters
for i := 0; i < len(s); i++ {
    fmt.Printf("%d: %x\n", i, s[i])  // Shows raw bytes
}

// Iterating by RUNES — correct for characters
for i, r := range s {
    fmt.Printf("byte %d: %c (U+%04X)\n", i, r, r)
}
// byte 0: H (U+0048)
// byte 1: e (U+0065)
// byte 2: l (U+006C)
// byte 3: l (U+006C)
// byte 4: o (U+006F)
// byte 5:   (U+0020)
// byte 6: 🌍 (U+1F30D)  — starts at byte 6, spans 4 bytes

// Correct character count
fmt.Println(utf8.RuneCountInString(s))  // 7 (not 10)

সাধারণ String Operation

import "strings"

s := "Hello, World!"

strings.Contains(s, "World")        // true
strings.HasPrefix(s, "Hello")       // true
strings.HasSuffix(s, "!")           // true
strings.ToUpper(s)                  // "HELLO, WORLD!"
strings.ToLower(s)                  // "hello, world!"
strings.TrimSpace("  hello  ")      // "hello"
strings.Split("a,b,c", ",")        // ["a", "b", "c"]
strings.Join([]string{"a","b"}, "-") // "a-b"
strings.ReplaceAll(s, "World", "Go") // "Hello, Go!"
strings.Count(s, "l")              // 3
strings.Index(s, "World")          // 7
strings.Repeat("ha", 3)            // "hahaha"

// Fields splits on any whitespace (better than Split for parsing)
strings.Fields("  foo   bar  baz ")  // ["foo", "bar", "baz"]

String Building: Performance ব্যাপারটা গুরুত্বপূর্ণ

+ দিয়ে string concatenation প্রতিবারই একটা নতুন string তৈরি করে (string immutable)। loop-এর ভেতর string বানাতে হলে strings.Builder ব্যবহার করুন:

// BAD: O(n²) — copies the entire string each iteration
func badConcat(items []string) string {
    result := ""
    for _, s := range items {
        result += s + ","  // Allocates a new string every time
    }
    return result
}

// GOOD: O(n) — writes to an internal buffer
func goodConcat(items []string) string {
    var sb strings.Builder
    for i, s := range items {
        if i > 0 {
            sb.WriteByte(',')
        }
        sb.WriteString(s)
    }
    return sb.String()
}

// BEST for simple join: use strings.Join
result := strings.Join(items, ",")

Benchmark পার্থক্য: 1000 item-এর জন্য, + concatenation লাগে ~500μs আর ~500 allocation। strings.Builder লাগে ~5μs আর ~8 allocation। অর্থাৎ 100x দ্রুত

Bytes বনাম Strings

[]byte হলো string-এর mutable কাজিন। এদের মধ্যে convert করলে data কপি হয়:

s := "hello"
b := []byte(s)    // Copies "hello" into a mutable byte slice
b[0] = 'H'        // Can modify bytes
s2 := string(b)   // Copies back to string: "Hello"

// bytes package mirrors strings package
import "bytes"

data := []byte("Hello, World!")
bytes.Contains(data, []byte("World"))
bytes.ToUpper(data)
bytes.Split(data, []byte(","))

// bytes.Buffer for building byte sequences
var buf bytes.Buffer
buf.WriteString("Hello")
buf.WriteByte(' ')
buf.WriteString("World")
result := buf.Bytes()  // []byte("Hello World")

কখন কোনটা ব্যবহার করবেন:

  • string — যে text বদলানো উচিত নয় (JSON key, log message, user display)
  • []byte — যে data আপনাকে modify করতে হবে, binary data, I/O buffer
  • strings.Builder — string ধাপে ধাপে বানানোর জন্য
  • bytes.Buffer — byte sequence বানানো, io.Writer implement করা

String Conversion-এর ফাঁদ

// Converting number to string does NOT give you the decimal representation
s := string(65)    // "A" (treats 65 as a Unicode code point)
s := string(128522) // "😊"

// Use strconv for number-to-string conversion
s := strconv.Itoa(65)         // "65"
s := strconv.FormatFloat(3.14, 'f', 2, 64)  // "3.14"
s := fmt.Sprintf("%d", 65)   // "65" (slower but more flexible)

// Parsing strings to numbers
n, err := strconv.Atoi("42")              // 42
f, err := strconv.ParseFloat("3.14", 64)  // 3.14
b, err := strconv.ParseBool("true")       // true

বাস্তব উদাহরণ: User Input Sanitize করা

func sanitizeUsername(input string) (string, error) {
    // Trim whitespace
    input = strings.TrimSpace(input)

    // Check length in runes (not bytes) for international names
    runeCount := utf8.RuneCountInString(input)
    if runeCount < 2 || runeCount > 30 {
        return "", fmt.Errorf("username must be 2-30 characters, got %d", runeCount)
    }

    // Validate each rune
    for i, r := range input {
        if !unicode.IsLetter(r) && !unicode.IsDigit(r) && r != '_' && r != '-' {
            return "", fmt.Errorf("invalid character at position %d: %c", i, r)
        }
    }

    return strings.ToLower(input), nil
}

// Works correctly with international text
sanitizeUsername("Ähmed_123")   // "ähmed_123", nil
sanitizeUsername("用户名")       // "用户名", nil
sanitizeUsername("ab")          // "ab", nil
sanitizeUsername("a")           // error: too short

মূল শিক্ষা

  1. len(s) byte গোনে, character নয় — character count-এর জন্য utf8.RuneCountInString ব্যবহার করুন
  2. string-এর উপর range rune ধরে iterate করে, index access s[i] byte দেয় — text-এর জন্য range ব্যবহার করুন
  3. loop-এ concatenation-এর জন্য strings.Builder ব্যবহার করুন+-এর চেয়ে 100x দ্রুত
  4. string(65) হলো "A", "65" নয় — number format করতে strconv.Itoa ব্যবহার করুন
  5. mutable data-এর জন্য []byte, immutable text-এর জন্য string — conversion data কপি করে
  6. character classification-এর জন্য unicode package ব্যবহার করুন — unicode.IsLetter, unicode.IsDigit