Golang String
1. Introduction
In Golang, strings are essentially immutable byte slices that can store diverse data types beyond traditional text formats like UTF-8. When indexing a string, you directly access individual bytes, demonstrating Golang’s straightforward approach. Unicode code points are represented by runes, which are crucial for multilingual support. Golang’s optimized string manipulation techniques, including efficient UTF-8 rune decoding within range loops, ensure high-performance execution. For practical examples and best practices, refer to the official Golang website to effectively leverage these powerful string capabilities, enabling developers of all levels to write cleaner and more efficient code.
2. Basics of Golang Strings
2.1. Definition of a string in Golang
In Golang, a string is a series of characters enclosed by either double quotes (“) or backticks (`). Strings are a crucial data type in Golang and serve the purpose of representing text-based data. The variable hello is defined as a string and set with the specific value “Hello, World!”.
var hello string = "Hello, World!"
In this example, the variable hello is declared as a string and initialized with the value “Hello, World!”.
2.2. Strings are immutable in nature
The strings in the Golang are immutable in nature. This indicates that once a string is created, its contents cannot be changed. This characteristic makes the string thread safe and shareable across multiple goroutines.
var s string = "Gopher"
s[0] = 'J' // This will not compile, because strings are immutable
In the above example, we try to update the first character of the string s by assigning a new value to s[0].However, this will not compile as strings are immutable in nature
2.3. Interpreted String Literals and Raw String Literals in Golang
Golang provides two types of string literals: interpreted string literals and raw string literals.
2.3.1. Interpreted String Literals
Interpreted string literals use double quotes (“). They can contain escape sequences, such as \n for a newline or \t for a tab.
Golang code :
var s string = "Hello\nWorld!"
In this example, the string s includes a newline character (\n) between the words “Hello” and “World!”.
2.3.2. Raw String Literals
Raw string literals use backticks (`). They do not interpret escape sequences and are useful when you need to include special characters in your string, such as backslashes or quotes.
Golang code :
var s string = `Hello\nWorld!`
In this example, the string s includes a literal backslash (\) followed by the letter “n”, rather than a newline character.
2.4. Creating and initializing strings
There are several ways to create and initialize strings in Golang:
2.4.1. Using interpreted string literals and raw literals
You can create a string using an interpreted string literal and raw string literals, as shown earlier.
package main
import "fmt"
func main() {
// Using interpreted string literals
interPretedStringLiterals := "Hello\tGo !"
fmt.Println(interPretedStringLiterals) // Output: Hello Go !
// Using raw literals
rawLiterals := `Hello\tGo`
fmt.Println(rawLiterals) // Output: Hello\tGo
}
2.4.2. Example of use of Escape Sequence in interpreted string Literal
func main() {
// Newline escape sequence (\n)
fmt.Println("Hello\nWorld")
// Tab escape sequence (\t)
fmt.Println("Hello\tWorld")
// Backslash escape sequence (\\)
fmt.Println("Path: C:\\Users\\Documents")
// Single quote escape sequence (\')
fmt.Println("It's raining")
// Double quote escape sequence (\")
fmt.Println("She said, \"Hello!\"")
// Unicode escape sequence (\u) for the Thank you symbol (🙏)
fmt.Println("Thank you symbol: \U0001F64F")
}
Output :
Hello
World
Hello World
Path: C:\Users\Documents
It's raining
She said, "Hello!"
Thank you symbol: 🙏
2.5. Difference between interpreted string literals and raw literals
Following table shows the differences between string literals and raw string literals in Golang:
Feature
Interpreted String Literals
Raw String Literals
3. String Functions in Golang
The strings package in Golang offers numerous functions for handling UTF-8 encoded strings. These functions enable you to perform typical string operations like searching, replacing, splitting, joining, and trimming. As part of Golang’s standard library, the strings package is extensively used for string manipulation in Golang programs.
Commonly Used String Functions
3.1. strings.Contains
Syntax: strings.Contains(s, substr string) bool
Purpose: To check if a substring is present within a string.
Parameters:
- s (string): The string to be searched.
- substr (string): The substring to search for within s.
Return Values:
- bool: Returns true if substr is found within s, otherwise returns false.
Example:
func main() {
exist := strings.Contains("hello, world", "world")
if !exist {
fmt.Println("String does not contain the substring")
} else {
fmt.Println("String contains the substring")
}
}
For the complete program please visit our GitHub repository
3.2. strings.HasPrefix and strings.HasSuffix
Purpose: strings.HasPrefix determines if a string begins with a specified prefix, while strings.HasSuffix checks if a string concludes with a specified suffix.
Parameters:
For strings.HasPrefix(s, prefix string) bool:
- s (string): The string to be examined.
- prefix (string): The prefix to search for at the start of s.
For strings.HasSuffix(s, suffix string) bool:
- s (string): The string to be examined.
- suffix (string): The suffix to search for at the end of s.
Return Values:
For strings.HasPrefix(s, prefix string) bool
- bool: Returns true if s starts with prefix, otherwise returns false.
For strings.HasSuffix(s, suffix string) bool
- bool: Returns true if s ends with suffix, otherwise returns false.
Example
func main() {
isPrefix := strings.HasPrefix("hello, world", "hello")
if !isPrefix {
fmt.Println("The string does not contain the substring at start")
} else {
fmt.Println(isPrefix)
}
isSuffix := strings.HasSuffix("hello, world", "world")
if !isSuffix {
fmt.Println("The string does not contain the substring at end")
} else {
fmt.Println(isSuffix)
}
}
// Output: true
// Output: true
For the complete program, please refer to our GitHub repository
3.3.strings.Replace
Purpose: The strings.Replace function is used to replace parts of a string with another substring.
Syntax: strings.Replace(s, old, new string, n int) string
Parameters:
- s (string): The original string to modify.
- old (string): The substring to be replaced.
- new (string): The substring that will replace the old one.
- n (int): The number of replacements to make. If n is less than 0, all instances are replaced. If n is 0, no replacements are made. If n is greater than 0, only the first n instances from the left are replaced.
Return Values:
- string: A new string with the specified replacements applied.
Example:
func main() {
fmt.Println(strings.Replace("hello, Gophers!", "o", "0", -1))
// Output: hell0, G0phers!
}
For the complete program, please refer to our GitHub repository
3.4 strings.Split and strings.Join
Syntax :
- strings.Split(s string, sep string) []string
- strings.Join(elements []string, sep string) string
Purpose:Â
The function strings.Split takes a string and a separator as input and returns a slice of substrings.The function strings.Join takes a slice of strings and a separator as input and returns a single string by joining the elements together.
Parameters:
For strings.Split
- s (string): The string to be split is referred to as s,Â
- sep (string): The separator string defining where to split s is referred to as sep
For strings.Join
- a []string: The slice of strings to be joined is referrxed to as a []string
- sep string:The separator string to insert between elements of a is referred to as sep.
Return Values:
For strings.Split
- []string: A slice of substrings resulting from splitting s by sep.
For strings.Join
- string: A single string formed by concatenating the elements of a, separated by sep.
Example:
func main() {
sliceOfSubstrings := strings.Split("One,Two,Three", ",")
fmt.Println(sliceOfSubstrings) // Output: [One Two Three]
string := strings.Join([]string{"One", "Two", "Three"}, "-")
fmt.Println(string) // One-Two-Three
}
For the complete program, please refer to our GitHub repository
3.5. strings.SplitAfter
Syntax: func SplitAfter(s, sep string) []string
Purpose: The SplitAfter function splits a string into a slice of substrings, breaking at each occurrence of a specified separator. The separator is included at the end of each substring.
Parameters:
- s: This is the string that will be split.
- sep: This is the string that indicates where the split will happen.
Return Values:
- The return values consist of a string slice that includes the substrings of s following each instance of sep. Each substring in the resulting slice includes the separator at its end. If the separator is not present in the string, the function will deliver a slice containing the original string.
Example:
func main() {
str := "hello,world,how,are,you"
sep := ","
result := strings.SplitAfter(str, sep)
fmt.Println(result)
}
//[hello, world, how, are, you]
For the complete program, please refer to our GitHub repository .
3.6. strings.SplitAfterN(s, sep string, n int) []string
Purpose:
SplitAfterN splits the string s into substrings after each instance of the separator sep, stopping after n splits or when no more instances of sep are found, whichever comes first.
Parameters:
- s: The string to split.
- sep: The separator string.
- n: Maximum number of splits to perform. A negative value means no limit on the number of splits.
Return:
- []string: A slice of substrings.
Example:
func main() {
str := "apple-orange-pear-grape-melon"
// Split after each '-' with a maximum of 3 splits
parts := strings.SplitAfterN(str, "-", 3)
fmt.Println("Split parts:", parts)
}
//output : Split parts: [apple- orange- pear- grape-melon]
For the complete program please visit our GitHub repository
3.7. strings.ToLower and strings.ToUpper
Syntax:
- strings.ToLower(s string) string
- strings.ToUpper(s string) string
Purpose: strings.ToLower converts all Unicode letters in a string to their lowercase equivalent. strings.ToUpper converts all Unicode letters in a string to their uppercase equivalent.
Parameters:
In the case of the strings.ToLower
- s (string): The input string to be converted to lowercase.
In the case of the strings.ToUpper
- s (string): The input string to be converted to uppercase
Return Values:
In the case of the strings.ToLower
- string: A new string with all characters in lowercase.
In the case of the strings.ToUpper
- string: A new string with all characters in uppercase.
Example:
func main() {
fmt.Println(strings.ToLower("HELLO, WORLD")) // Output: hello, world
fmt.Println(strings.ToUpper("hello, world")) // Output: HELLO, WORLD
}
For the complete program, please refer to our GitHub repository.Â
3.8. strings.Trim, strings.TrimSpace, strings.TrimPrefix, strings.TrimSuffix, strings.TrimRight and strings.TrimLeft
Syntax :
- strings.Trim(s, cutset string) string
- strings.TrimSpace(s string) string
- strings.TrimPrefix(s string, prefix string) string
- strings.TrimSuffix(s string, suffix string) string
- strings.TrimLeft(s string, left string) string
- strings.TrimRight(s string, right string) string
Description:Â
- strings.Trim removes leading and trailing characters from s that are included in the cutset string.Â
ParameterÂ
- s: The input string to be trimmed.Â
- cutset: A string containing the characters to be trimmed from the right end of s
Return Values: A new string with all leading and trailing characters specified in cutset removed from s.
2. strings.TrimSpace removes all leading and trailing white space (spaces, tabs, newlines) from s. It ensures that the string s does not have any extraneous whitespace at its start or end.
Parameters:
- s: The input string to be trimmed.
Return Values: A new string with all leading and trailing white space removed from s.
3. strings.TrimPrefix removes the specified prefix from the start of s, if s begins with prefix. If s does not start with a prefix, s remains unchanged.
Parameters:
- s: The input string to be trimmed.
- prefix: The prefix to be removed from the start of s.
Return Values: A new string with the specified prefix removed from the start of s. If s does not start with a prefix, the original string s is returned unchanged.
4. strings.TrimSuffix removes the specified suffix from the end of s, if s ends with suffix. If s does not end with a suffix, s remains unchanged.
Parameters:
- s: The input string to be trimmed.
- suffix: The suffix to be removed from the end of s.
Return Values: A new string with the specified suffix removed from the end of s. If s does not end with a suffix, the original string s is returned unchanged.
5. strings.TrimLeft removes all leading characters from s that are included in the left string. It continues trimming until encountering a character not in left.
Parameters:
- s: The input string to be trimmed.
- cutset: A string containing the characters to be trimmed from the left end of s.
Return Values: A new string with all leading characters specified in cutset removed from s.
6. strings.TrimRight removes all trailing characters from s that are included in the right string. It continues trimming until encountering a character not in right.
Parameters:
- s: The input string to be trimmed.
- cutset: A string containing the characters to be trimmed from the right end of s.
Return Values: A new string with all trailing characters specified in cutset removed from s.
Example:
func main() {
fmt.Println(strings.Trim("!!!hello, world!!!", "!")) // Output: hello, world
fmt.Println(strings.TrimSpace(" \t\n hello, world \n\t\r\n")) // Output: hello, world
fmt.Println(strings.TrimPrefix("hello, world", "hello")) // Output: , world
fmt.Println(strings.TrimSuffix("hello, world", "world")) // Output: hello,
fmt.Println(strings.TrimLeft("@@@@cool, world!!!", "@")) // Output: cool, world!!!
fmt.Println(strings.TrimRight("!!!hello, world!!!", "!")) // Output: !!!hello, world
}
For the complete program, please visit to our GitHub repositoryÂ
4. String Manipulation in Golang
4.1 Concatenation using + and fmt.Sprintf
Concatenation using +:
Parameters:
- message1: The first string.
- message2: The second string.
Parameters:
- message1: The first string.
- message2: The second string.
Return Values:
- A new string that results from combining message1 and message2.
Example:
func main() {
message1 := "Golang has"
message2 := "built-in-concurrency support"
result := message1 + " " + message2
fmt.Println(result) // Output: Golang has built-in-concurrency support
}
For the complete program, please visit our GitHub repository.Â
Concatenation using fmt.Sprintf:
Syntax: result := fmt.Sprintf(format, a…)
Purpose: Concatenates strings by formatting them using the fmt.Sprintf function.
Parameters:
format: A format string that specifies how to format the values.
a…: A variadic parameter representing the values to format.
Return Values: A formatted string.
Example:
func main() {
str1 := "Hello"
str2 := "World"
result := fmt.Sprintf("%s %s", str1, str2)
fmt.Println(result) // Output: Hello World
}
For the complete program, please visit our GitHub repository.Â
4.2. Using strings.Builder for Efficient Concatenation:
func main() {
var builder strings.Builder
builder.WriteString("Hello")
builder.WriteString(", ")
builder.WriteString("Go")
result := builder.String()
fmt.Println(result) // Output: Hello, Go
}
4.3 Substring Extraction using Slicing
Syntax: substring := str[start:end]
Purpose: Extracts a segment of characters from a string using slicing notation.
Parameters:
- str: The original string from which to extract the substring.
- start: The index where the substring extraction begins (inclusive).
- end: The index where the substring extraction ends (exclusive).
Return Value: A new string containing characters from index start to end-1 of the original string str.
func main() {
//literals -> H e l l o , W o r l d
//Index -> 0 1 2 3 4 5 6 7 8 9 10 11
str := "Hello, World"
substring := str[7:12]
fmt.Println(substring) // Output: World
}
5. String conversion
For detail  information on string conversion, please visit our blog
6. String Formatting
String formatting is a crucial aspect of Golang programming, enabling developers to construct and output strings with precise formatting. The fmt package in Golang provides a robust set of functions and formatting verbs to handle various formatting needs.
6.1 Using fmt.Sprintf
Syntax: fmt.Sprintf(format string, a …interface{}) string
Purpose: Formats a string according to a specified format and returns the resulting string.
Parameters:
- format: A string that contains text interspersed with format specifiers.
- a: A variadic parameter of interface type, which holds the values to be formatted and inserted into the format string.
Return Values: A string formatted according to the format specifiers and corresponding values in a.
Example:
func main() {
name := "Alice"
age := 30
formattedString := fmt.Sprintf("Name: %s, Age: %d", name, age)
fmt.Println(formattedString)
// Output: Name: Alice, Age: 30
}
For the complete program please visit our GitHub repository
6.2. Using fmt.Fprintf
Syntax: fmt.Fprintf(w io.Writer, format string, a …interface{}) (int, error)
Purpose: Formats a string according to a specified format and writes it to the provided writer.
Parameters:
- w: An io.Writer where the formatted string will be written.
- format: A string that contains text interspersed with format specifiers.
- a: A variadic parameter of interface type, which holds the values to be formatted and inserted into the format string.
Return Values:
- The number of bytes written.
- Any error encountered during the write operation.
Example:
func main() {
name := "Alice"
age := 30
n, err := fmt.Fprintf(os.Stdout, "Name: %s, Age: %d\n", name, age)
if err != nil {
fmt.Println("Error:", err)
}
fmt.Println("Bytes written:", n)
}
For detailed information regarding the formatting verb please visit fmt packageÂ
6.3 String Comparison
- strings using ==, <, >, etc.
- Description: Direct comparison operators (==, <, >, <=, >=) can be used to compare strings lexicographically in Go.
func main() {
str1 := "apple"
str2 := "banana"
// Direct comparison
if str1 == str2 {
fmt.Println("Strings are equal")
} else if str1 < str2 {
fmt.Println("str1 is less than str2")
} else {
fmt.Println("str1 is greater than str2")
}
}
For the complete program please visit our GitHub repository
6.4 Using strings.Compare
Description: The strings.Compare function compares two strings lexicographically and returns an integer that indicates their ordering: 0 if equal, -1 if the first is less, and 1 if the first is greater.
Example:
func main() {
result := strings.Compare("Gopher", "Gopher")
switch result {
case 0:
fmt.Println("Strings are equal")
case -1:
fmt.Println("First string is less than the second")
case 1:
fmt.Println("First string is greater than the second")
}
}
For the complete program, please refer to our GitHub repositoryÂ
6.5. Case-insensitive comparisons with strings.EqualFold
Description:
The strings.EqualFold function compares two strings while ignoring differences in letter case. It returns true if the strings are considered equal under Unicode case-folding rules; otherwise, it returns false.
- Description: strings.EqualFold performs a case-insensitive comparison of two strings and returns true if they are equal under Unicode case-folding, false otherwise.
Example:
func main() {
equal := strings.EqualFold("GoLang", "golang")
if equal {
fmt.Println("The strings are equal, ignoring case")
} else {
fmt.Println("The strings are not equal")
}
}
These methods provide flexibility for comparing strings based on specific requirements, accommodating both case-sensitive and case-insensitive comparisons.
7. Iterating Over Strings
7.1. Iterating over string characters using range
The range keyword in Golang allows iterating over strings character by character. Each iteration yields the Unicode code point of the character and its byte index.
func main() {
str := "Hello, Golang"
// Iterating over characters using range
for index, runeValue := range str {
fmt.Printf("Character at index %d is %c\n", index, runeValue)
}
}
Output:
Character at index 0 is H
Character at index 1 is e
Character at index 2 is l
Character at index 3 is l
Character at index 4 is o
Character at index 5 is ,
Character at index 6 is
Character at index 7 is G
Character at index 8 is o
Character at index 9 is l
Character at index 10 is a
Character at index 11 is n
Character at index 12 is g
When iterating over strings that contain Unicode characters, the indexing may not correspond directly to individual characters due to the strings being encoded in UTF-8. Let us understand this with the following example :
func main() {
str := "🙏🙏"
// Iterate over the string using range
for index, runeValue := range str {
fmt.Printf("Character at index %d is %c\n", index, runeValue)
}
}
Output:
Character at index 0 is 🙏
Character at index 4 is 🙏
For the complete program please visit our GitHub repository
Inconsistency in the indexing can be mitigated by using following ways:
7.1.1 Use []rune Conversion:
func main() {
str := "🙏🙏"
runestr := []rune(str)
// Iterating over characters and printing Unicode code points
for index, runeValue := range runestr {
fmt.Printf("value:%c\tindexed at %d\n", runeValue, index)
}
}
Output:
value:🙏 indexed at 0
value:🙏 indexed at 1
For the complete program please visit our GitHub repository
7.1.2. Use utf8 Package:
func main() {
str := "🙏🙏"
// Counting runes in the string
runeCount := utf8.RuneCountInString(str)
for i := 0; i < runeCount; i++ {
// Decode the next rune in the string
runeValue, size := utf8.DecodeRuneInString(str)
// Print the index and the decoded rune
fmt.Printf("index: %d, character: %c\n", i, runeValue)
// Move to the next rune in the string by slicing out the bytes of the decoded rune
str = str[size:]
}
}
Output:
index: 0, character: 🙏
index: 1, character: 🙏
7.2. Iterating over string characters using range
Description: Golang handles Unicode characters natively, allowing strings to contain multi-byte characters. When iterating over strings, each iteration provides the Unicode code point of the character, regardless of its byte length.
func main() {
str := "Hello, Golang"
// Iterating over characters and printing Unicode code points
for index, runeValue := range str {
fmt.Printf("Unicode code point: U+%04X indexed at %d\n", runeValue, index)
}
}
Output:
Unicode code point: U+0048
Unicode code point: U+0065
Unicode code point: U+006C
Unicode code point: U+006C
Unicode code point: U+006F
Unicode code point: U+002C
Unicode code point: U+0020
Unicode code point: U+0047
Unicode code point: U+006F
Unicode code point: U+006C
Unicode code point: U+0061
Unicode code point: U+006E
Unicode code point: U+0067
8. Regular Expressions with Strings
Regular expressions, also known as regex, are available in Golang through the regexp package. This package offers features for working with regular expressions. Below is a quick overview of how to use regular expressions in Golang.
Example 1: Checking Pattern Existence with Regular Expression
// example 1 checking pattern existance
// Define a regular expression pattern to match "hello"
pattern := regexp.MustCompile(`hello`)
testString1 := "say hello to everyone"
testString2 := "goodbye for now"
// Check if each string contains "hello" and print results
fmt.Printf("'%s' contains 'hello': %v\n", testString1, pattern.MatchString(testString1))
fmt.Printf("'%s' contains 'hello': %v\n", testString2, pattern.MatchString(testString2))
Output:
say hello to everyone' contains 'hello': true
'goodbye for now' contains 'hello': false
In the above example, a regular expression pattern is defined to match with strings. If the teststring matches the pattern, the MatchString() function returns true.
Example 2: Extracting Operator Symbols from Text
text := "Let's extract symbols like +, -, *, /, =, and ! from this text."
// Define a regular expression pattern to match programming symbols
symbolPattern := regexp.MustCompile(`[+\-*/=!]+`)
// Find all symbols in the text using the pattern
symbols := symbolPattern.FindAllString(text, -1)
// Print the extracted symbols
fmt.Println("Extracted symbols:")
for _, symbol := range symbols {
fmt.Println(symbol)
}
Output:
Extracted symbols:
+
-
*
/
=
!
In the above example, regular expression pattern is defined to match operators within a text. By utilizing the FindAllString() function, a slice of strings is returned, containing all matches.
For the above complete program, please refer to our GitHub repositoryÂ
For detailed information of regular expressions package please visit to regular expression’s official documentation and for regular expression syntax please visit to regular expression syntax’s official documentation
9. Performance Considerations
9.1. Efficient String Concatenation using strings.Builder
In Golang, concatenating strings using the + operator can be inefficient, especially with large strings or frequent concatenations. This approach creates new strings, leading to increased memory allocation and garbage collection overhead.
To address this, Golang offers the strings.Builder type, which acts as a mutable string buffer for efficient concatenation. By utilizing strings.Builder, you can concatenate strings without generating intermediate strings, thereby reducing memory allocation and improving performance.
Example:
func main() {
var b strings.Builder
b.WriteString("Hello, ")
b.WriteString("world!")
result := b.String() // Efficient concatenation
fmt.Println(result)
}
9.2. Memory Management and Optimization Tips
Efficient memory management is crucial when working with strings to avoid performance degradation. Consider the following tips:
- Avoid Unnecessary String Allocations: Prefer strings.Builder or bytes.Buffer for concatenation over the + operator.
- Reuse Strings: Reuse existing strings whenever possible instead of creating new ones.
- Avoid Large String Allocations: Break down large strings into smaller segments to minimize memory usage.
- Prefer string over []byte: Use the string type when manipulating strings to minimize unnecessary memory allocations.
10. Best Practices
10.1.String-related code guidelines
- it’s essential to use variable names that clearly indicate the string’s purpose, When code involves strings,
- Creating constants for frequently used strings in your code can improve readability and maintainability.Â
- It’s best to avoid complex string operations and instead break them down into smaller, more manageable functions. Rather than manually combining strings, consider utilizing string formatting functions such as fmt.Sprintf and strconv.Format for creating formatted strings.
10.2. Points to remember
- Avoid using + for concatenation: Use strings.Builder or bytes.Buffer for efficient concatenation.
- Don’t assume string immutability: Be aware that strings can be modified accidentally using indexing or slicing.
- Watch out for Unicode issues: Be mindful of Unicode characters and encoding when working with strings.
10.3. String issues debugging
- Use the fmt package: Use fmt.Println or fmt.Printf to inspect string values during debugging.
- Use a debugger: Use a debugger like dlv to step through your code and inspect string values.
- Test string functions thoroughly: Write comprehensive tests for string-related functions to catch errors and edge cases.
10.4. Examples and Use Cases
Here are some practical examples and real-world use cases for string operations in Golang:
Practical examples of string operations
- String concatenation: Concatenate strings using strings.Builder or bytes.Buffer.
- String formatting: Use fmt.Sprintf to create formatted strings.
- String searching: Use strings.Contains or strings.Index to search for substrings.
- String manipulation: Use strings.Replace or strings.Split to manipulate strings.
10.5. Reference links
- Golang documentation: strings, fmt
- Golang blog: Strings in Go