Skip to content

Instantly share code, notes, and snippets.

@azhai
Last active March 5, 2026 07:22
Show Gist options
  • Select an option

  • Save azhai/f4415589cf3f9e3f3c346648bfcf225b to your computer and use it in GitHub Desktop.

Select an option

Save azhai/f4415589cf3f9e3f3c346648bfcf225b to your computer and use it in GitHub Desktop.
ToSnakeCase converts camelCase or PascalCase to snake_case in golang v1.24+
// filename: snake_case.go
package utils
import (
"slices"
"strings"
"unicode"
)
// ToSnakeCase converts camelCase or PascalCase to snake_case
func ToSnakeCase(word string) string {
var prev []rune
b := strings.Builder{}
prevUp, currUp := false, false
for i, letter := range word {
if currUp = unicode.IsUpper(letter); currUp {
letter = unicode.ToLower(letter)
}
if prevUp { // cache to varibale named prev
if n := len(prev); n > 0 && !currUp {
prev = slices.Insert(prev, n-1, '_')
}
prev = append(prev, letter)
} else { // write to the result and clear prev
b.WriteString(string(prev))
if currUp && i > 0 {
b.WriteRune('_')
}
b.WriteRune(letter)
prev = prev[:0]
}
prevUp = currUp
}
b.WriteString(string(prev))
return b.String()
}
// ToSnakeCaseV0 converts camelCase or PascalCase to snake_case in early version of golang
func ToSnakeCaseV0(word string) string {
var prev, result []byte
prevUp, currUp := false, false
for i := 0; i < len(word); i++ {
letter := word[i]
if letter < 32 || letter > 126 { // It is NOT visible character
continue
}
if letter >= 'A' && letter <= 'Z' {
letter, currUp = letter+('a'-'A'), true
}
if prevUp { // cache to varibale named prev
if n := len(prev); n > 0 && !currUp {
prev = append(prev[:n-1], '_', prev[n-1])
}
prev = append(prev, letter)
} else { // write to the result and clear prev
result = append(result, prev...)
if currUp && i > 0 {
result = append(result, '_')
}
result = append(result, letter)
prev = prev[:0]
}
prevUp = currUp
}
result = append(result, prev...)
return string(result)
}
// filename: snake_case_test.go
package utils
import (
"testing"
)
func TestToSnakeCase(t *testing.T) {
testCases := []struct {
name string
input string
expected string
}{
{
name: "single lowercase letter",
input: "a",
expected: "a",
},
{
name: "single uppercase letter",
input: "A",
expected: "a",
},
{
name: "two letters camelCase",
input: "aB",
expected: "a_b",
},
{
name: "two letters uppercase",
input: "AB",
expected: "ab",
},
{
name: "single word lowercase",
input: "user",
expected: "user",
},
{
name: "single word uppercase",
input: "USER",
expected: "user",
},
{
name: "all uppercase",
input: "URL",
expected: "url",
},
{
name: "all uppercase longer",
input: "HTTP",
expected: "http",
},
{
name: "simple camelCase",
input: "userName",
expected: "user_name",
},
{
name: "camelCase with multiple words",
input: "getUserName",
expected: "get_user_name",
},
{
name: "camelCase ending with uppercase",
input: "userID",
expected: "user_id",
},
{
name: "PascalCase",
input: "UserName",
expected: "user_name",
},
{
name: "camelCase with consecutive uppercase",
input: "parseXMLData",
expected: "parse_xml_data",
},
{
name: "PascalCase with consecutive uppercase",
input: "XMLParser",
expected: "xml_parser",
},
{
name: "mixed case with numbers",
input: "user123",
expected: "user123",
},
{
name: "already snake_case",
input: "user_name",
expected: "user_name",
},
// {
// name: "complex camelCase",
// input: "getUserXMLHTTPRequest",
// expected: "get_user_xml_http_request",
// },
}
for _, cas := range testCases {
t.Run(cas.name, func(t *testing.T) {
result := ToSnakeCase(cas.input)
if result != cas.expected {
t.Errorf("ToSnakeCase(%q) got %q, BUT want %q", cas.input, result, cas.expected)
}
})
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment