How to check if a string contains a substring in Go
Use strings.Contains(s, substr) — it returns a bool and is literally strings.Index(s, substr) >= 0. It is case-sensitive, and there is no ContainsFold, so for a case-insensitive search you lowercase both sides yourself.
Go has no in operator and no String.includes method — substring searching lives in the strings package, and it is one function call. What trips people up is everything around it: Contains matches anywhere (not just at the end), ContainsAny takes a set of characters rather than a substring, indexes come back as byte offsets, and case-insensitivity is your job. Each example below runs on this page — hit Run, then edit the code and run it again.
1strings.ContainsRecommended
strings.Contains(s, substr) reports whether substr appears anywhere inside s. It never allocates, never returns an error, and on the standard toolchain it drops into assembly-optimised byte search (falling back to Rabin–Karp for long needles), so it is the fastest thing you can reach for. This is the answer to the question 99% of the time.
Output
Prints true, false, true, then this is a Go page. The third line is the one that bites: an empty needle is contained in everything, so if substr comes from user input, guard it with substr != "" before you treat a true as a real match. Looking for an element in a []string instead? That's checking if a slice contains an element.
2HasPrefix and HasSuffix — anchored checks
Contains answers "anywhere", which is the wrong question for file extensions, URL schemes and path roots. strings.HasSuffix(name, ".png") anchors the match to the end and strings.HasPrefix to the start — both are O(len of the affix) and stop immediately on the first mismatch. Since Go 1.20, strings.CutPrefix and CutSuffix test and strip in one call.
Output
The fourth line prints true false — logo.png.txt is a text file that merely mentions .png, and only HasSuffix gets that right. The last line prints logo.png true. For real filenames prefer filepath.Ext(path) == ".png", and lowercase first if the extension may be .PNG.
3ContainsAny, ContainsRune and ContainsFunc
These three answer a different question: not "is this sequence in there" but "is any character of this kind in there". ContainsAny(s, chars) treats its second argument as a set of runes, not a substring; ContainsRune(s, r) checks a single code point; and ContainsFunc(s, f) (Go 1.21+) takes a predicate, so any unicode.IsDigit-style function works.
Output
Output is true, false, true, false, true. Note the asymmetry on line 4: Contains(s, "") is true but ContainsAny(s, "") is false — an empty set has nothing to match. Don't reach for ContainsAny(s, "abc") expecting it to find "abc"; that's Contains.
4Case-insensitive search (there is no ContainsFold)
The strings package ships EqualFold but no ContainsFold, and that omission is deliberate: case folding can change a string's length (German ß folds to SS), so a genuinely fold-aware search is not a byte scan and can't share Contains' fast path. The practical answer is to lowercase both sides and search that — and to know exactly where it lies to you.
Output
Lines print false, true, true, false, true, false. Line 4 is the trap people fall into: EqualFold(s, "disk") is false because EqualFold compares entire strings — it is == ignoring case, not a search. Use it on a slice you already found (line 5). And line 6 shows the limit of the ToLower trick: BALIK lowercases to balik, which is not the Turkish balık. Two ToLower calls also allocate two new strings per check, so hoist them out of hot loops.
5Index and LastIndex — when you need the position
Contains throws away information. If you need to know where the match is, call strings.Index (first occurrence) or strings.LastIndex (last one); both return -1 when there is no match, which is why the >= 0 comparison is the whole implementation of Contains. strings.Count answers "how many".
Output
Prints 0, 12, -1, true, go | examples/go/strings, 2, then 2 3 5. That last line is the gotcha: in café! the ! is at index 5, not 4, because indexes are byte offsets and é takes two bytes. Slicing on an index from Index is always safe though — in valid UTF-8 a byte search can never land mid-rune. Prefer strings.Cut over the Index-then-slice dance when you are splitting on a separator.
6Which should you use?
| Method | What it matches | Cost | Best for |
|---|---|---|---|
| strings.Contains | A literal, anywhere | Fastest | The default check |
| strings.HasPrefix / HasSuffix | A literal, anchored to an end | Fastest | Extensions, schemes, path roots |
| strings.ContainsAny | Any one rune from a set | Fast | "Does it have punctuation?" |
| strings.ContainsRune / ContainsFunc | One rune, or a predicate | Fast | Single char, unicode.IsDigit |
| strings.Index / LastIndex | A literal, and tells you where | Same as Contains | When you need the offset |
| Contains(ToLower(s), ToLower(sub)) | A literal, ignoring case | Two allocations | Case-insensitive search |
| regexp.MatchString | A pattern, not a literal | Slowest | Word boundaries, classes, (?i) |
7regexp — when the needle is a pattern
If what you are looking for is a shape rather than a fixed string — a ticket id, a whole word, a case-insensitive match — regexp is the tool. MatchString is the Contains of the regexp world, and the (?i) flag gives you the case-insensitive search the strings package won't. Compile the pattern once at package level: MustCompile inside a hot function recompiles on every call.
Output
Prints true, false, true, ABC-1234, [7 15], then 1\.5\.0. Line 2 is why you'd bother: Contains(s, "error") would match errors, but \b anchors to word boundaries. The flip side is cost — Go's RE2 engine is linear-time and never blows up, but it is still an order of magnitude slower than strings.Contains. If your needle is a literal, don't use regexp; the QuoteMeta line shows how much of it you'd have to escape anyway.
Frequently asked questions
Does Go have an "in" operator for strings?
No. Go has no in operator and no includes method on strings — substring checks live in the standard library as strings.Contains(s, substr), which returns a bool. Import strings and call it; there is nothing to install.
Is strings.Contains case-sensitive?
Yes. strings.Contains("Error: Disk Full", "disk full") is false. There is no strings.ContainsFold, so the usual fix is strings.Contains(strings.ToLower(s), strings.ToLower(substr)), or a regexp with the (?i) flag. Note that strings.EqualFold compares two whole strings for equality — it does not search inside one.
What is the difference between strings.Contains and strings.Index?
Contains is implemented as Index(s, substr) >= 0. Index returns the byte offset of the first occurrence, or -1 if there is none, so use Index when you need the position (and LastIndex for the final occurrence) and Contains when a bool reads better.
Why does strings.Contains(s, "") return true?
Because the empty string is a substring of every string, including the empty string itself — Index returns 0 for it. If the needle comes from user input, check substr != "" first. strings.ContainsAny(s, "") behaves the opposite way and returns false, because an empty set of characters has nothing to match.
Is strings.Contains safe for UTF-8 and emoji?
Yes. It searches bytes, but UTF-8 is self-synchronising: a valid multi-byte sequence can never match part of another character, so a byte-level hit is always a real character-level hit and it is safe to slice at the returned index. What is not rune-aware is case folding — for that, lowercase both sides or use a regexp.
What is the difference between Contains and ContainsAny in Go?
Contains(s, substr) looks for the substring as a sequence. ContainsAny(s, chars) treats its second argument as a set of runes and reports whether any single one of them appears, so ContainsAny("hunter2!", "!@#$%") is true. Use ContainsRune for one code point and ContainsFunc (Go 1.21+) for a predicate such as unicode.IsDigit.