Regexp #

Regular expression adalah alat yang sangat powerful untuk pencocokan pola, validasi, dan ekstraksi teks — tapi juga salah satu yang paling mudah disalahgunakan. Package regexp di Go mengimplementasikan sintaks RE2, yang berbeda dari PCRE (Perl-Compatible Regular Expressions) yang umum digunakan di bahasa lain: RE2 menjamin waktu eksekusi linear O(n) terhadap panjang input, artinya tidak ada yang namanya “catastrophic backtracking” yang bisa membuat program hang saat menerima input berbahaya. Ini membuat regex di Go aman digunakan dalam aplikasi produksi yang menerima input dari pengguna. Artikel ini membahas cara menggunakan regexp dengan efektif — dari kompilasi dan pencocokan dasar, hingga grup capture, penggantian dinamis, dan kapan sebaiknya tidak menggunakan regex.

Gambaran Besar Package regexp #

flowchart TD
    R["package regexp"] --> Compile["Kompilasi Regex"]
    R --> Match["Pencocokan"]
    R --> Find["Pencarian & Ekstraksi"]
    R --> Replace["Penggantian"]
    R --> Split["Pemisahan"]

    Compile --> C1["regexp.Compile(pattern)\nreturn *Regexp, error"]
    Compile --> C2["regexp.MustCompile(pattern)\npanic jika error\nuntuk pattern konstan"]
    Compile --> C3["regexp.CompilePOSIX\nsemantics POSIX"]

    Match --> M1["re.MatchString(s)\nbool — cocok atau tidak"]
    Match --> M2["re.Match([]byte)\nbool — untuk []byte"]

    Find --> F1["re.FindString\ncocok pertama"]
    Find --> F2["re.FindAllString\nsemua cocok"]
    Find --> F3["re.FindStringSubmatch\ncocok + grup capture"]
    Find --> F4["re.FindAllStringSubmatch\nsemua + grup capture"]
    Find --> F5["re.FindStringIndex\nposisi cocok pertama"]

    Replace --> RP1["re.ReplaceAllString\nganti semua cocok"]
    Replace --> RP2["re.ReplaceAllLiteralString\ntanpa $ expansion"]
    Replace --> RP3["re.ReplaceAllStringFunc\nganti dengan fungsi"]

    Split --> SP1["re.Split\npecah berdasarkan pola"]

    style R fill:#4f86c6,color:#fff
    style Compile fill:#e8f5e9
    style Match fill:#e3f2fd
    style Find fill:#fff3e0
    style Replace fill:#f3e5f5
    style Split fill:#fce4ec

Kompilasi Regex #

Sebelum bisa digunakan, pola regex harus dikompilasi menjadi objek *regexp.Regexp. Kompilasi ini mahal — selalu lakukan sekali saja, biasanya sebagai variabel package:

package main

import (
    "fmt"
    "regexp"
)

// BENAR: kompilasi sekali sebagai variabel package (level global)
var (
    reEmail    = regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
    reNomorHP  = regexp.MustCompile(`^(\+62|62|0)8[1-9][0-9]{6,9}$`)
    reKodePos  = regexp.MustCompile(`^\d{5}$`)
    reURLPath  = regexp.MustCompile(`^/[a-zA-Z0-9/\-._~:@!$&'()*+,;=%?#]*$`)
)

// ANTI-PATTERN: kompilasi di dalam fungsi yang sering dipanggil
func validasiEmailBuruk(email string) bool {
    re := regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
    // re dikompilasi setiap kali fungsi dipanggil — sangat lambat!
    return re.MatchString(email)
}

// BENAR: gunakan variabel yang sudah dikompilasi
func validasiEmail(email string) bool {
    return reEmail.MatchString(email)
}

func main() {
    // Compile — kembalikan error jika pola tidak valid
    re, err := regexp.Compile(`(\w+)`)
    if err != nil {
        fmt.Println("pola regex tidak valid:", err)
        return
    }
    fmt.Println(re.FindString("Halo Dunia")) // Halo

    // MustCompile — panic jika pola tidak valid
    // Gunakan HANYA untuk pola yang kamu tulis sendiri (bukan dari input user!)
    re2 := regexp.MustCompile(`\d+`)
    fmt.Println(re2.FindString("abc 123 def")) // 123

    // JANGAN: MustCompile dengan input pengguna
    // userPattern := os.Stdin.ReadString('\n')
    // re3 := regexp.MustCompile(userPattern) // akan panic jika input tidak valid!

    // BENAR untuk input pengguna: Compile dan tangani error
    // re3, err := regexp.Compile(userPattern)
}

Sintaks Regex Go (RE2) #

Go menggunakan sintaks RE2, bukan PCRE. Perbedaan paling penting: tidak ada lookahead/lookbehind dan tidak ada backreference:

flowchart LR
    subgraph Dasar["Karakter Dasar"]
        D1[". — karakter apapun kecuali newline"]
        D2["\\d — digit [0-9]"]
        D3["\\w — word char [a-zA-Z0-9_]"]
        D4["\\s — whitespace"]
        D5["\\D \\W \\S — negasi dari atas"]
    end

    subgraph Kuantifier["Kuantifier"]
        K1["* — 0 atau lebih"]
        K2["+ — 1 atau lebih"]
        K3["? — 0 atau 1"]
        K4["{n} — tepat n"]
        K5["{n,m} — n sampai m"]
        K6["{n,} — minimal n"]
        K7["*? +? ?? — non-greedy"]
    end

    subgraph Anchor["Anchor"]
        A1["^ — awal string (atau baris dengan (?m))"]
        A2["$ — akhir string (atau baris dengan (?m))"]
        A3["\\b — word boundary"]
        A4["\\A — awal string (selalu)"]
        A5["\\z — akhir string (selalu)"]
    end

    subgraph Group["Grup & Alternatif"]
        G1["(abc) — capturing group"]
        G2["(?:abc) — non-capturing group"]
        G3["(?P<nama>abc) — named capturing group"]
        G4["a|b — alternatif: a atau b"]
        G5["[abc] — karakter set"]
        G6["[^abc] — negasi karakter set"]
    end

    subgraph Flag["Flag"]
        F1["(?i) — case-insensitive"]
        F2["(?m) — multiline (^ dan $ cocok per baris)"]
        F3["(?s) — . cocok dengan newline juga"]
        F4["(?U) — non-greedy default"]
    end
// Contoh sintaks regex
re := regexp.MustCompile(`\d+`)           // satu atau lebih digit
re2 := regexp.MustCompile(`[a-z]+`)       // satu atau lebih huruf kecil
re3 := regexp.MustCompile(`(foo|bar)`)    // "foo" atau "bar"
re4 := regexp.MustCompile(`\b\w+\b`)      // kata utuh
re5 := regexp.MustCompile(`(?i)hello`)    // "hello" case-insensitive
re6 := regexp.MustCompile(`(?m)^\d+`)     // digit di awal setiap baris
re7 := regexp.MustCompile(`(?s).+`)       // semua karakter termasuk newline
re8 := regexp.MustCompile(`(?P<tahun>\d{4})-(?P<bulan>\d{2})-(?P<hari>\d{2})`)

// Karakter yang perlu di-escape dalam regex:
// . * + ? ^ $ {} [] | () \
// Gunakan regexp.QuoteMeta untuk escape otomatis
userInput := "harga $10.99 (diskon)"
pattern := regexp.QuoteMeta(userInput) // "harga \\$10\\.99 \\(diskon\\)"
re9 := regexp.MustCompile(pattern)
fmt.Println(re9.MatchString(userInput)) // true

MatchString — Pencocokan Dasar #

var reEmail = regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)

// MatchString — apakah string cocok dengan pola?
fmt.Println(reEmail.MatchString("[email protected]"))  // true
fmt.Println(reEmail.MatchString("bukan-email"))        // false
fmt.Println(reEmail.MatchString("@nodomain.com"))      // false

// Fungsi top-level regexp.MatchString — kompilasi setiap panggilan, hindari di produksi
matched, err := regexp.MatchString(`^\d+$`, "12345")
fmt.Println(matched, err) // true <nil>

// Match untuk []byte
data := []byte("Halo 123")
re := regexp.MustCompile(`\d+`)
fmt.Println(re.Match(data)) // true

FindString — Mencari Teks yang Cocok #

re := regexp.MustCompile(`\d+`)
teks := "Saya punya 3 kucing dan 12 anjing"

// FindString — kembalikan kecocokan pertama (string kosong jika tidak ada)
fmt.Println(re.FindString(teks)) // "3"

// FindAllString — kembalikan semua kecocokan
semua := re.FindAllString(teks, -1) // -1 = tidak ada batas
fmt.Println(semua) // [3 12]

// Batasi jumlah kecocokan
dua := re.FindAllString(teks, 2)
fmt.Println(dua) // [3 12]

// FindStringIndex — posisi (start, end) kecocokan pertama
idx := re.FindStringIndex(teks)
fmt.Println(idx) // [11 12] — posisi "3"
fmt.Println(teks[idx[0]:idx[1]]) // "3"

// FindAllStringIndex — posisi semua kecocokan
semua2 := re.FindAllStringIndex(teks, -1)
fmt.Println(semua2) // [[11 12] [22 24]]

// FindReaderIndex — baca dari io.Reader
reader := strings.NewReader(teks)
idxR := re.FindReaderIndex(reader)
fmt.Println(idxR) // [11 12]

Grup Capture — Ekstraksi Bagian Tertentu #

Grup capture (...) memungkinkan ekstraksi bagian tertentu dari teks yang cocok:

flowchart LR
    Pattern["Pattern:\n(?P<tahun>\\d{4})-(?P<bulan>\\d{2})-(?P<hari>\\d{2})"] --> Input["Input: '2024-03-15'"]

    Input --> SM["FindStringSubmatch"]
    SM --> R0["[0] '2024-03-15'\nkeseluruhan match"]
    SM --> R1["[1] '2024'\ngrup 1: tahun"]
    SM --> R2["[2] '03'\ngrup 2: bulan"]
    SM --> R3["[3] '15'\ngrup 3: hari"]

    SM --> Named["SubexpIndex('tahun')\n→ indeks grup bernama"]

    style Pattern fill:#4f86c6,color:#fff
    style R0 fill:#e8f5e9
    style R1 fill:#e3f2fd
    style R2 fill:#fff3e0
    style R3 fill:#f3e5f5
// Pattern dengan named capturing group
reTanggal := regexp.MustCompile(
    `(?P<tahun>\d{4})-(?P<bulan>\d{2})-(?P<hari>\d{2})`)

// FindStringSubmatch — kembalikan [seluruh match, grup1, grup2, ...]
match := reTanggal.FindStringSubmatch("Tanggal: 2024-03-15")
if match != nil {
    fmt.Println("Seluruh match:", match[0]) // "2024-03-15"
    fmt.Println("Tahun:", match[1])          // "2024"
    fmt.Println("Bulan:", match[2])          // "03"
    fmt.Println("Hari:", match[3])           // "15"

    // Akses grup bernama dengan SubexpIndex
    tahunIdx := reTanggal.SubexpIndex("tahun")
    bulanIdx := reTanggal.SubexpIndex("bulan")
    hariIdx := reTanggal.SubexpIndex("hari")

    fmt.Printf("Tahun=%s, Bulan=%s, Hari=%s\n",
        match[tahunIdx], match[bulanIdx], match[hariIdx])
}

// Lebih idiomatis: buat map dari named groups
func submatchMap(re *regexp.Regexp, s string) map[string]string {
    match := re.FindStringSubmatch(s)
    if match == nil {
        return nil
    }

    result := make(map[string]string)
    for i, name := range re.SubexpNames() {
        if i != 0 && name != "" {
            result[name] = match[i]
        }
    }
    return result
}

// Penggunaan
fields := submatchMap(reTanggal, "2024-03-15")
fmt.Println(fields["tahun"]) // "2024"
fmt.Println(fields["bulan"]) // "03"
fmt.Println(fields["hari"])  // "15"

// FindAllStringSubmatch — semua kecocokan beserta grup
reAngka := regexp.MustCompile(`(\d+)\.(\d+)`)
teks := "Harga: 15.000 dan 250.000"
semua := reAngka.FindAllStringSubmatch(teks, -1)
for _, m := range semua {
    fmt.Printf("Match: %s, Bagian: %s.%s\n", m[0], m[1], m[2])
}
// Match: 15.000, Bagian: 15.000
// Match: 250.000, Bagian: 250.000

ReplaceAll — Penggantian Teks #

re := regexp.MustCompile(`\d+`)

// ReplaceAllString — ganti semua kecocokan dengan string tetap
hasil := re.ReplaceAllString("abc 123 def 456", "NUM")
fmt.Println(hasil) // "abc NUM def NUM"

// ReplaceAllString dengan referensi grup ($1, $2, ...)
reNama := regexp.MustCompile(`(\w+)\s+(\w+)`)
hasil2 := reNama.ReplaceAllString("Budi Santoso", "$2, $1")
fmt.Println(hasil2) // "Santoso, Budi"

// ReplaceAllLiteralString — tidak memproses $ sebagai referensi grup
hasil3 := re.ReplaceAllLiteralString("abc 123 def", "$1")
fmt.Println(hasil3) // "abc $1 def" — $ tidak di-expand

// ReplaceAllStringFunc — ganti dengan hasil fungsi
hasil4 := re.ReplaceAllStringFunc("abc 123 def 456", func(match string) string {
    n, _ := strconv.Atoi(match)
    return strconv.Itoa(n * 2) // gandakan setiap angka
})
fmt.Println(hasil4) // "abc 246 def 912"

// Contoh: topeng nomor kartu kredit
reKartuKredit := regexp.MustCompile(`\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b`)
teksLog := "Pembayaran dengan kartu 4111 1111 1111 1111 berhasil"
teksAman := reKartuKredit.ReplaceAllStringFunc(teksLog, func(match string) string {
    // Hanya tampilkan 4 digit terakhir
    bersih := regexp.MustCompile(`[\s-]`).ReplaceAllString(match, "")
    return "**** **** **** " + bersih[len(bersih)-4:]
})
fmt.Println(teksAman) // "Pembayaran dengan kartu **** **** **** 1111 berhasil"

// ReplaceAllFunc untuk []byte
re2 := regexp.MustCompile(`[aeiou]`)
hasil5 := re2.ReplaceAllFunc([]byte("halo dunia"), func(b []byte) []byte {
    return bytes.ToUpper(b)
})
fmt.Println(string(hasil5)) // "hAlO dUnIA"

Split — Memisahkan Teks #

// Split berdasarkan pola regex
re := regexp.MustCompile(`[\s,;]+`) // whitespace atau koma atau titik koma

teks := "apel, mangga; jeruk    durian"
buah := re.Split(teks, -1) // -1 = tidak ada batas
fmt.Println(buah) // [apel mangga jeruk durian]

// Batasi jumlah bagian
bagian := re.Split("a,b,c,d,e", 3)
fmt.Println(bagian) // [a b c,d,e] — maksimal 3 bagian

// Split dengan grup capture — separator disertakan
reSeparator := regexp.MustCompile(`(\d+)`)
hasil := reSeparator.Split("abc123def456ghi", -1)
fmt.Println(hasil) // [abc def ghi] — angka (separator) tidak disertakan

// Untuk menyertakan separator, gunakan FindAllStringIndex manual

Pola Validasi yang Umum #

// Kumpulan validator yang siap pakai
var (
    // Email — sederhana, bukan RFC 5322 yang kompleks
    ValidEmail = regexp.MustCompile(
        `^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)

    // Nomor HP Indonesia
    ValidNomorHP = regexp.MustCompile(
        `^(\+62|62|0)(811|812|813|821|822|823|851|852|853|` +
        `814|815|816|855|856|857|858|` +
        `817|818|819|859|877|878|` +
        `831|832|833|838|` +
        `895|896|897|898|899|` +
        `881|882|883|884|885|886|887|888|889)\d{5,8}$`)

    // NIK (Nomor Induk Kependudukan) — 16 digit
    ValidNIK = regexp.MustCompile(`^\d{16}$`)

    // NPWP — format XX.XXX.XXX.X-XXX.XXX
    ValidNPWP = regexp.MustCompile(
        `^\d{2}\.\d{3}\.\d{3}\.\d-\d{3}\.\d{3}$`)

    // Kode Pos Indonesia
    ValidKodePos = regexp.MustCompile(`^[1-9]\d{4}$`)

    // Plat Nomor Kendaraan (sederhana)
    ValidPlatNomor = regexp.MustCompile(
        `(?i)^[A-Z]{1,2}\s?\d{1,4}\s?[A-Z]{1,3}$`)

    // Slug URL
    ValidSlug = regexp.MustCompile(`^[a-z0-9]+(?:-[a-z0-9]+)*$`)

    // UUID v4
    ValidUUID = regexp.MustCompile(
        `^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$`)

    // IPv4
    ValidIPv4 = regexp.MustCompile(
        `^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$`)

    // Warna hex CSS (#RGB atau #RRGGBB)
    ValidHexColor = regexp.MustCompile(`^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$`)
)

func validasiInput(email, hp, nik string) []string {
    var errors []string

    if !ValidEmail.MatchString(email) {
        errors = append(errors, "format email tidak valid")
    }
    if !ValidNomorHP.MatchString(hp) {
        errors = append(errors, "nomor HP tidak valid")
    }
    if !ValidNIK.MatchString(nik) {
        errors = append(errors, "NIK harus 16 digit")
    }

    return errors
}

Pola Parsing yang Umum #

// Parse log format: [2024-03-15 14:30:00] LEVEL message
reLog := regexp.MustCompile(
    `\[(?P<waktu>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]\s+` +
    `(?P<level>\w+)\s+(?P<pesan>.+)`)

func parseLogLine(baris string) (waktu, level, pesan string, ok bool) {
    m := reLog.FindStringSubmatch(baris)
    if m == nil {
        return "", "", "", false
    }

    names := reLog.SubexpNames()
    for i, name := range names {
        switch name {
        case "waktu":
            waktu = m[i]
        case "level":
            level = m[i]
        case "pesan":
            pesan = m[i]
        }
    }
    return waktu, level, pesan, true
}

// Parse URL sederhana
reURL := regexp.MustCompile(
    `(?P<skema>https?)://(?P<host>[^/:]+)(?::(?P<port>\d+))?(?P<path>/[^?#]*)?(?:\?(?P<query>[^#]*))?(?:#(?P<fragment>.*))?`)

// Parse CSV sederhana (tanpa quoting)
reCsv := regexp.MustCompile(`,\s*`)

func parseCSVBaris(baris string) []string {
    return reCsv.Split(baris, -1)
}

// Ekstraksi semua URL dari HTML
reLink := regexp.MustCompile(`href="([^"]+)"`)

func ekstraksiLink(html string) []string {
    matches := reLink.FindAllStringSubmatch(html, -1)
    links := make([]string, 0, len(matches))
    for _, m := range matches {
        links = append(links, m[1]) // m[1] adalah grup capture pertama
    }
    return links
}

Performa: Kapan Tidak Menggunakan Regex #

flowchart TD
    Q{"Apa yang perlu\ndilakukan?"} --> Simple["Operasi sederhana:\ncek prefix/suffix,\ncari substring,\nsplit dengan delimiter tetap"]
    Q --> Complex["Pencocokan pola kompleks,\nformat tidak teratur,\nbanyak variasi"]

    Simple --> NoRegex["Gunakan strings package\nLEBIH CEPAT dan LEBIH JELAS"]
    Complex --> Regex["Pertimbangkan Regex\n(tapi ukur dulu)"]

    NoRegex --> NS1["strings.HasPrefix / HasSuffix"]
    NoRegex --> NS2["strings.Contains / Index"]
    NoRegex --> NS3["strings.Split / Fields"]
    NoRegex --> NS4["strings.TrimSpace / Trim"]

    Regex --> RA1["Validasi format: email, URL, tanggal"]
    Regex --> RA2["Ekstraksi pola dari teks bebas"]
    Regex --> RA3["Penggantian pola yang kompleks"]

    style NoRegex fill:#e8f5e9
    style Regex fill:#e3f2fd
// Perbandingan performa untuk operasi sederhana

// LAMBAT: regex untuk operasi sederhana
rePrefix := regexp.MustCompile(`^http://`)
rePrefix.MatchString(url) // overhead kompilasi + matching engine

// CEPAT: strings package untuk operasi sederhana
strings.HasPrefix(url, "http://") // langsung, tidak ada overhead

// LAMBAT: regex untuk split dengan delimiter tetap
reKoma := regexp.MustCompile(`,`)
reKoma.Split(csv, -1)

// CEPAT: strings.Split
strings.Split(csv, ",")

// Benchmark tipikal:
// strings.HasPrefix: ~5 ns/op
// regexp.MatchString: ~150 ns/op (30x lebih lambat untuk kasus sederhana)
// Untuk pola kompleks, perbedaannya tidak signifikan karena regex memang diperlukan

// Kapan regex MEMANG diperlukan:
// - Format yang sangat bervariasi (tanggal dalam berbagai format)
// - Validasi yang kompleks (email, URL dengan banyak edge case)
// - Ekstraksi dari teks tidak terstruktur
// - Penggantian dengan kondisi kompleks

Keamanan: Regex dari Input Pengguna #

// BERBAHAYA: MustCompile dengan input pengguna
func cariDenganPola(teks, polaDariUser string) []string {
    re := regexp.MustCompile(polaDariUser) // PANIC jika pola tidak valid!
    return re.FindAllString(teks, -1)
}

// AMAN: Compile dengan penanganan error
func cariDenganPolaAman(teks, polaDariUser string) ([]string, error) {
    // Batasi panjang pola untuk mencegah ReDoS (tidak berlaku untuk RE2,
    // tapi tetap praktik baik untuk membatasi kompleksitas)
    if len(polaDariUser) > 1000 {
        return nil, fmt.Errorf("pola regex terlalu panjang")
    }

    re, err := regexp.Compile(polaDariUser)
    if err != nil {
        return nil, fmt.Errorf("pola regex tidak valid: %w", err)
    }

    return re.FindAllString(teks, 100), nil // batasi jumlah hasil juga
}

// CATATAN: Go menggunakan RE2 yang TIDAK rentan terhadap ReDoS
// (Catastrophic Backtracking) karena dijamin O(n).
// Berbeda dengan PCRE di PHP, Python, Java yang bisa hang dengan pola tertentu.
// Namun tetap batasi pola dari pengguna sebagai praktik keamanan yang baik.

Pola Penggunaan di Produksi #

Middleware Sanitasi Input #

var (
    reDangerousChars = regexp.MustCompile(`[<>'"&;]`)
    reMultipleSpaces  = regexp.MustCompile(`\s{2,}`)
    reNonAlphanumeric = regexp.MustCompile(`[^a-zA-Z0-9\s]`)
)

func sanitasiInput(input string) string {
    // Hapus karakter berbahaya untuk HTML
    bersih := reDangerousChars.ReplaceAllString(input, "")
    // Normalisasi spasi berlebih
    bersih = reMultipleSpaces.ReplaceAllString(bersih, " ")
    return strings.TrimSpace(bersih)
}

func buatSlug(judul string) string {
    // Konversi ke lowercase
    slug := strings.ToLower(judul)
    // Ganti karakter non-alphanumeric dengan -
    slug = reNonAlphanumeric.ReplaceAllString(slug, "-")
    // Normalisasi multiple -
    reDash := regexp.MustCompile(`-+`)
    slug = reDash.ReplaceAllString(slug, "-")
    // Hapus - di awal dan akhir
    return strings.Trim(slug, "-")
}

// Penggunaan
fmt.Println(buatSlug("Panduan Go: Package Standard Library!"))
// panduan-go-package-standard-library

Parser Log dengan Named Groups #

// Format log Nginx: 192.168.1.1 - - [15/Mar/2024:14:30:00 +0700] "GET /api/v1 HTTP/1.1" 200 1234
var reNginxLog = regexp.MustCompile(
    `(?P<ip>\d+\.\d+\.\d+\.\d+) - - ` +
    `\[(?P<waktu>[^\]]+)\] ` +
    `"(?P<method>\w+) (?P<path>[^ ]+) HTTP/[\d.]+" ` +
    `(?P<status>\d+) (?P<bytes>\d+)`)

type NginxLogEntry struct {
    IP     string
    Waktu  string
    Method string
    Path   string
    Status int
    Bytes  int
}

func parseNginxLog(baris string) (*NginxLogEntry, error) {
    match := reNginxLog.FindStringSubmatch(baris)
    if match == nil {
        return nil, fmt.Errorf("format log tidak dikenali: %q", baris)
    }

    names := reNginxLog.SubexpNames()
    fields := make(map[string]string)
    for i, name := range names {
        if name != "" {
            fields[name] = match[i]
        }
    }

    status, _ := strconv.Atoi(fields["status"])
    bytes, _ := strconv.Atoi(fields["bytes"])

    return &NginxLogEntry{
        IP:     fields["ip"],
        Waktu:  fields["waktu"],
        Method: fields["method"],
        Path:   fields["path"],
        Status: status,
        Bytes:  bytes,
    }, nil
}

Redaksi Data Sensitif #

var (
    reKartuKredit = regexp.MustCompile(`\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b`)
    reNIK         = regexp.MustCompile(`\b\d{16}\b`)
    reEmail2      = regexp.MustCompile(`[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}`)
    reNomorHP2    = regexp.MustCompile(`\b0\d{9,11}\b`)
)

func redaksiDataSensitif(teks string) string {
    // Topeng kartu kredit
    teks = reKartuKredit.ReplaceAllStringFunc(teks, func(m string) string {
        bersih := regexp.MustCompile(`[\s-]`).ReplaceAllString(m, "")
        return "**** **** **** " + bersih[len(bersih)-4:]
    })

    // Topeng NIK
    teks = reNIK.ReplaceAllStringFunc(teks, func(m string) string {
        return m[:6] + "**********"
    })

    // Topeng email
    teks = reEmail2.ReplaceAllStringFunc(teks, func(m string) string {
        parts := strings.Split(m, "@")
        if len(parts) != 2 {
            return m
        }
        nama := parts[0]
        if len(nama) > 2 {
            nama = nama[:2] + strings.Repeat("*", len(nama)-2)
        }
        return nama + "@" + parts[1]
    })

    return teks
}

// Penggunaan
log := "NIK: 3273011234567890, Email: [email protected], Kartu: 4111 1111 1111 1111"
fmt.Println(redaksiDataSensitif(log))
// NIK: 327301**********, Email: bu*************@gmail.com, Kartu: **** **** **** 1111

Kapan Beralih ke Alternatif #

Tetap gunakan regexp jika:
  ✓ Validasi format yang kompleks: email, URL, tanggal dalam berbagai format
  ✓ Ekstraksi pola dari teks tidak terstruktur: log, HTML, dokumen
  ✓ Penggantian pola yang kompleks dengan kondisi dinamis
  ✓ Parsing format yang memiliki banyak variasi

Gunakan strings package jika:
  ✗ Mencari substring tetap → strings.Contains, strings.Index
  ✗ Memisahkan dengan delimiter tetap → strings.Split
  ✗ Memeriksa prefix/suffix → strings.HasPrefix, strings.HasSuffix
  ✗ Menghapus whitespace → strings.TrimSpace
  ✗ Mengganti string tetap → strings.ReplaceAll
  (semua ini 10-100x lebih cepat dari regex yang setara)

Gunakan strconv jika:
  ✗ Validasi angka → strconv.Atoi, strconv.ParseFloat
  ✗ Parsing angka dari string → lebih tepat dan jelas

Pertimbangkan parser khusus jika:
  ✗ Parsing HTML/XML → golang.org/x/net/html atau encoding/xml
  ✗ Parsing JSON → encoding/json
  ✗ Parsing YAML → gopkg.in/yaml.v3
  ✗ Parsing format yang sangat kompleks → parser terstruktur lebih maintainable

Ringkasan #

  • Kompilasi regex sekali sebagai variabel package dengan MustCompile — jangan kompilasi di dalam fungsi yang dipanggil berulang kali, overhead-nya signifikan.
  • MustCompile hanya untuk pattern konstan yang kamu tulis sendiri — untuk pattern dari input pengguna, selalu gunakan Compile dan tangani errornya.
  • Go menggunakan RE2, bukan PCRE — tidak ada lookahead/lookbehind, tidak ada backreference, tapi dijamin aman dari ReDoS karena kompleksitas O(n).
  • Named capturing groups dengan (?P<nama>...) dan SubexpIndex("nama") membuat kode lebih mudah dibaca daripada mengakses match[1], match[2] secara numerik.
  • ReplaceAllStringFunc untuk penggantian dinamis — gunakan saat nilai pengganti bergantung pada isi match, seperti transformasi angka atau penyamaran data.
  • regexp.QuoteMeta(s) untuk menggunakan string literal sebagai pattern — ini penting saat pola mengandung karakter khusus regex dari input pengguna.
  • Ukur sebelum menggunakan regex — untuk operasi sederhana seperti cek prefix atau split dengan delimiter tetap, strings package 10-100× lebih cepat.
  • FindAllStringSubmatch mengembalikan slice of slices — result[i][0] adalah match ke-i, result[i][1] adalah grup pertama match ke-i.
  • Kompilasi regex tidak aman untuk concurrent use — tapi eksekusi *regexp.Regexp yang sudah dikompilasi aman digunakan dari banyak goroutine sekaligus.

← Sebelumnya: Filepath   Berikutnya: Encoding Csv →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact