Skip to content

strings and time

The strings package provides functions for manipulating UTF-8 encoded strings. Strings in Go are immutable byte slices — all operations return new strings rather than modifying in place.

strings.Contains("hello world", "world") // true
strings.HasPrefix("hello", "hel") // true
strings.HasSuffix("hello", "llo") // true
strings.Index("hello", "ll") // 2
strings.LastIndex("hello", "l") // 3
strings.Count("hello", "l") // 2

Index returns -1 if the substring is not found. Count counts non-overlapping occurrences.

parts := strings.Split("a,b,c", ",") // ["a", "b", "c"]
parts := strings.SplitN("a,b,c", ",", 2) // ["a", "b,c"]
result := strings.Join(parts, "-") // "a-b-c"
fields := strings.Fields(" hello world ") // ["hello", "world"]

SplitN limits the number of splits. Fields splits on whitespace and discards empty strings.

strings.TrimSpace(" hello ") // "hello"
strings.Trim("!!!hello!!!", "!") // "hello"
strings.TrimPrefix("hello", "hel") // "lo"
strings.TrimSuffix("hello", "llo") // "hel"
strings.ToUpper("hello") // "HELLO"
strings.ToLower("HELLO") // "hello"
strings.Replace("foo bar foo", "foo", "baz", 1) // "baz bar foo"
strings.ReplaceAll("foo bar foo", "foo", "baz") // "baz bar baz"
strings.Repeat("ab", 3) // "ababab"

Replace takes a count argument — -1 replaces all occurrences (same as ReplaceAll).

For efficient string concatenation in loops, use strings.Builder instead of repeated +=:

var b strings.Builder
for _, s := range items {
b.WriteString(s)
b.WriteByte(",')
}
result := b.String()

Builder avoids allocating a new string on each concatenation. Grow(n) pre-allocates capacity:

var b strings.Builder
b.Grow(1000)
for _, s := range items {
b.WriteString(s)
}

The strconv package converts between strings and other types:

n, err := strconv.Atoi("42") // int 42
s := strconv.Itoa(42) // "42"
s := strconv.FormatBool(true) // "true"
s := strconv.FormatInt(-42, 10) // "-42"
s := strconv.FormatInt(255, 16) // "ff"
s := strconv.FormatFloat(3.14, 'f', 2, 64) // "3.14"
f, err := strconv.ParseFloat("3.14", 64) // 3.14
i, err := strconv.ParseInt("ff", 16, 64) // 255
s := strconv.Quote("hello \"world\"") // "hello \"world\""
s, err := strconv.Unquote(`"hello"`) // "hello"

Atoi is shorthand for ParseInt(s, 10, 0). Always check the returned error.

t := time.Now() // current local time
t := time.Date(2026, 5, 30, 14, 30, 0, 0, time.UTC)
fmt.Println(t.Year()) // 2026
fmt.Println(t.Month()) // May
fmt.Println(t.Day()) // 30
fmt.Println(t.Hour()) // 14
fmt.Println(t.Minute()) // 30
fmt.Println(t.Second()) // 0
fmt.Println(t.Unix()) // seconds since epoch
fmt.Println(t.UnixNano()) // nanoseconds since epoch
fmt.Println(t.Weekday()) // Saturday
fmt.Println(t.IsZero()) // false
layout := "2006-01-02 15:04:05"
t, err := time.Parse(layout, "2026-05-30 14:30:00")

Go uses a reference date for layout strings. The reference date is Mon Jan 2 15:04:05 MST 2006 (which is 01/02 03:04:05 PM 06 -0700 in a more memorable form). Each component determines how the corresponding value is interpreted.

Common layouts:

time.RFC3339 // "2006-01-02T15:04:05Z07:00"
time.RFC3339Nano // "2006-01-02T15:04:05.999999999Z07:00"
time.RFC1123 // "Mon, 02 Jan 2006 15:04:05 MST"
time.Kitchen // "3:04PM"
time.RubyDate // "2006-01-02 15:04:05 -0700"
time.UnixDate // "Mon Jan 2 15:04:05 MST 2006"
t := time.Now()
fmt.Println(t.Format(time.RFC3339)) // "2026-05-30T14:30:00Z"
fmt.Println(t.Format("2006-01-02")) // "2026-05-30"
fmt.Println(t.Format("Jan _2, 2006")) // "May 30, 2026"
fmt.Println(t.Format("15:04:05")) // "14:30:00"

time.Duration is an int64 representing nanoseconds. Constants help construct durations:

d := 2 * time.Hour + 30 * time.Minute // 2h30m0s
d := time.Millisecond * 500 // 500ms
d := time.Duration(5) * time.Second // 5s
fmt.Println(d) // "2h30m0s"
fmt.Println(d.Hours()) // 2.5
fmt.Println(d.Minutes()) // 150
fmt.Println(d.Seconds()) // 9000
fmt.Println(d.Milliseconds()) // 9000000
t := time.Now()
future := t.Add(24 * time.Hour) // 24 hours from now
past := t.Add(-24 * time.Hour) // 24 hours ago
diff := future.Sub(t) // 24h0m0s
fmt.Println(diff.Hours()) // 24
t1 := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
t2 := time.Date(2026, 12, 31, 0, 0, 0, 0, time.UTC)
t1.Before(t2) // true
t1.After(t2) // false
t2.Equal(t1) // false
since := time.Since(t1) // duration since t1
until := time.Until(t2) // duration until t2

A Timer fires once after a specified duration:

timer := time.NewTimer(2 * time.Second)
<-timer.C // blocks for 2 seconds, then receives the current time

Stop a timer before it fires:

timer := time.NewTimer(5 * time.Second)
stopped := timer.Stop()
if stopped {
fmt.Println("timer was stopped before firing")
}

If the timer has already fired or been stopped, Stop() returns false. Always drain the channel if Stop() returns false:

if !timer.Stop() {
<-timer.C
}

A Ticker fires repeatedly at a fixed interval:

ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for i := 0; i < 5; i++ {
<-ticker.C
fmt.Println("tick", time.Now().Format("15:04:05"))
}

Always stop tickers with defer ticker.Stop() to prevent goroutine leaks.

// After: fires once, returns a channel
<-time.After(2 * time.Second) // blocks for 2 seconds
// AfterFunc: fires once, calls a function
timer := time.AfterFunc(2*time.Second, func() {
fmt.Println("2 seconds elapsed")
})
timer.Stop() // cancel

time.After is convenient but note that it creates a timer that is not garbage-collected until it fires. Avoid time.After in tight loops — use time.NewTimer instead.

loc, err := time.LoadLocation("America/New_York")
if err != nil {
log.Fatal(err)
}
t := time.Date(2026, 5, 30, 14, 30, 0, 0, loc)
fmt.Println(t) // 2026-05-30 14:30:00 -0400 EDT
utc := t.UTC() // convert to UTC
local := t.Local() // convert to local
back := utc.In(loc) // convert to specific location

time.UTC is a pre-defined location for UTC. time.Local is the system’s local time zone.

Comparisons: always compare times in the same zone. Equal compares the absolute instant regardless of location:

t1 := time.Date(2026, 5, 30, 14, 0, 0, 0, time.UTC)
t2 := time.Date(2026, 5, 30, 10, 0, 0, 0, loc) // same instant, different zone
t1.Equal(t2) // true

Strings are immutable bricks, Builder is the mortar: In Go, strings are like sealed glass bottles — you can’t change the liquid inside, but you can glue bottles together (which creates a new bottle each time). strings.Builder is like a bucket where you pour liquids together, creating one final bottle at the end. The time package’s layout system is a fill-in-the-blank form: the reference date 01/02 03:04:05 PM '06 tells the parser which blank is month, which is day, which is hour.

Why it matters: String concatenation in loops with += creates O(n²) allocations. strings.Builder makes it O(n). Time parsing with the wrong layout silently produces wrong dates — the reference date convention prevents this once you internalize it.

The key insight: Go’s time layout uses a memorable reference moment (January 2, 3:04:05 PM 2006) so that every position in the format string corresponds to the value that belongs there.

  1. Using += for string concatenation in loops. Each += allocates a new string. Use strings.Builder for repeated concatenation.

  2. Not checking strconv errors. Atoi and ParseInt return errors for invalid input. Ignoring the error yields a zero value, which can silently corrupt data.

  3. Using the wrong layout string for time.Parse. The layout must use the reference date 2006-01-02 15:04:05. Using other values (e.g., 2015-01-02) causes incorrect parsing.

  4. Comparing times with ==. Use t1.Equal(t2) instead. Two time.Time values in different locations representing the same instant are not ==, but Equal returns true.

  5. Leaking timers and tickers. time.NewTimer and time.NewTicker create resources that must be stopped. Failing to call Stop() leaks a goroutine. Always use defer.

  6. Using time.After in a loop. Each call creates a new timer that cannot be stopped. In a loop, this leaks timers. Use time.NewTimer with Stop() instead.

  7. Ignoring time zone in parsing. time.Parse without a time zone offset produces a time with no location (UTC +0). Use time.ParseInLocation when the input has a known time zone but no offset:

loc, _ := time.LoadLocation("America/New_York")
t, _ := time.ParseInLocation("2006-01-02 15:04", "2026-05-30 14:30", loc)

This topic covers the core concepts of strings and time in Go, including underlying theory, practical implementation, and key applications.

Key concepts include:

  • string manipulation and efficient concatenation
  • type conversions with strconv
  • time parsing, formatting, and arithmetic
  • timers, tickers, and time zones
  • common pitfalls and best practices

Understanding these concepts thoroughly is essential for both examinations and practical programming, and requires both theoretical knowledge and hands-on practice.

Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.

  • I/O: Buffered I/O and stream processing for string manipulation.
  • net/http: HTTP header parsing and time-based cache headers.
  • Types and Variables: String and time type fundamentals.