strings.LastIndex(s, substr) liefert den Byte-Offset des letzten Vorkommens von substr in s. Ist substr nicht enthalten, kommt -1 zurück. Die Funktion ist das logische Pendant zu strings.Index: identische Signatur, identische Rückgabesemantik bei Treffer und Nicht-Treffer, aber die Suche zielt auf den am weitesten rechts liegenden Treffer. Genau das ist gewollt, wenn das relevante Vorkommen am Ende des Strings liegt — Dateiendungen hinter dem letzten Punkt, der Basename hinter dem letzten Slash, der letzte Trenner in einem CSV-Restfeld oder die rechte Grenze beim suffix-basierten Splitten.

Die Signatur ist die spiegelbildliche Variante zu Index. Zwei String-Parameter, ein int als Rückgabewert. Bei Treffer ist der Wert ein gültiger Byte-Offset im Bereich [0, len(s)-len(substr)], bei Nicht-Treffer exakt -1.

Go signatur.go
package main

import (
	"fmt"
	"strings"
)

func main() {
	s := "log-2026-05-01.txt log-2026-05-22.txt"

	fmt.Println(strings.LastIndex(s, "log-")) // letzter Treffer
	fmt.Println(strings.LastIndex(s, "xml"))  // kein Treffer
}
Output
19
-1

Der erste Aufruf findet den zweiten Logdatei-Präfix bei Byte 19, weil das die spätere Position von log- im String ist. Der zweite Aufruf liefert -1, weil xml nicht vorkommt — derselbe Sentinel wie bei Index, also reicht ein gemeinsamer Check if i < 0.

Semantisch sucht LastIndex von rechts nach links: der zurückgegebene Offset ist garantiert die größte Position, an der substr als Teilstring auftritt. Intern wählt die Implementierung je nach Länge von substr unterschiedliche Algorithmen — kurze Substrings nutzen Byte-Scans, längere ein rückwärts laufendes Rabin-Karp. Für den Aufrufer relevant ist nur das beobachtbare Verhalten.

Go richtung.go
package main

import (
	"fmt"
	"strings"
)

func main() {
	s := "ab--cd--ef--gh"

	fmt.Println(strings.Index(s, "--"))     // erstes Vorkommen
	fmt.Println(strings.LastIndex(s, "--")) // letztes Vorkommen
}
Output
2
10

Index bleibt am ersten -- direkt nach ab stehen, LastIndex läuft bis zum letzten Trenner vor gh. Beide Funktionen sehen den gleichen String, treffen aber genau gegenüberliegende Entscheidungen — das macht sie zum natürlichen Paar für Links/Rechts-Splitten.

Wie alle Index-Funktionen im strings-Paket gibt LastIndex einen Byte-Offset zurück, keinen Rune-Index. Das ist UTF-8-sicher: der Treffer liegt immer exakt auf einer Substring-Grenze, weil die Suche byteweise vergleicht — und gültige UTF-8-Sequenzen lassen sich nicht halbieren.

Go bytes.go
package main

import (
	"fmt"
	"strings"
)

func main() {
	s := "café · café · café"

	i := strings.LastIndex(s, "café")
	fmt.Println("offset:", i)
	fmt.Println("rest:  ", s[i:])
}
Output
offset: 16
rest:   café

café belegt 5 Bytes (é ist zweibytes), das Trennzeichen · weitere 5 Bytes. Der letzte Treffer liegt deshalb bei Byte 16, nicht bei Rune-Position 12. Wer Zeichen statt Bytes braucht, muss den Suffix s[i:] durch einen Rune-Counter (utf8.RuneCountInString) schieben.

Hier liegt die einzige bewusste Asymmetrie zu Index: LastIndex(s, "") gibt len(s) zurück, nicht 0. Die Logik dahinter: der leere Substring kommt „überall" vor, und die letzte Position davon ist hinter dem letzten Byte. Diese Konvention ist konsistent mit dem mathematischen Modell, in dem ein leerer String genau zwischen jedem Zeichen-Paar sitzt.

Go leer.go
package main

import (
	"fmt"
	"strings"
)

func main() {
	s := "hallo"

	fmt.Println(strings.Index(s, ""))     // Index: 0
	fmt.Println(strings.LastIndex(s, "")) // LastIndex: len(s)
	fmt.Println(strings.LastIndex("", "x"))
	fmt.Println(strings.LastIndex("", ""))
}
Output
0
5
-1
0

Wer LastIndex in einer Schleife verwendet, um Suffixe abzubauen, muss deshalb explizit auf substr == "" prüfen, sonst landet die Iteration in einer Endlos-Schleife bei len(s). LastIndex("", "x") ist -1 wie erwartet, LastIndex("", "") ist 0, weil len("") eben 0 ist.

Für Substrings ab Länge 2 nutzt die Standardbibliothek eine rückwärts laufende Rabin-Karp-Variante mit rollender Hash-Funktion. Die Komplexität ist linear in len(s) mit kleiner Konstante; das durchschnittliche Verhalten liegt nahe an einem einfachen Byte-Scan. Für Single-Byte-Substrings übernimmt LastIndexByte, das auf den meisten Architekturen SIMD-optimierten Maschinencode nutzt. Wer also nur ein einzelnes Trenn-Byte sucht, sollte direkt LastIndexByte aufrufen — das spart den Substring-Vergleich und den Hash-Setup.

FunktionSuchtSubstring-FormRückgabe bei Nicht-Treffer
Indexerstes Vork.beliebiger String-1
LastIndexletztes Vork.beliebiger String-1
LastIndexByteletztes Vork.einzelnes Byte-1
IndexByteerstes Vork.einzelnes Byte-1

Die Wahl fällt entlang zweier Achsen: Suchrichtung (erstes vs. letztes Vorkommen) und Pattern-Breite (Substring vs. einzelnes Byte). Für Pfad- und Endungs-Operationen ist fast immer die rechte Spalte gefragt, weil die Trenner einzelne Bytes sind.

In Skripten, die Dateinamen klassifizieren, reicht oft die Endung nach dem letzten Punkt. LastIndex(name, ".") liefert die Grenze, name[i+1:] den Suffix ohne den Punkt selbst. Für produktiven Code empfiehlt sich filepath.Ext, das Edge Cases wie versteckte Unix-Dateien (.bashrc) und Pfade mit Verzeichnis-Punkten korrekt behandelt — aber für simple In-Memory-Klassifizierung ist LastIndex direkter und vermeidet den Pfad-Parser.

Go endung.go
package main

import (
	"fmt"
	"strings"
)

func ext(name string) string {
	i := strings.LastIndex(name, ".")
	if i < 0 || i == len(name)-1 {
		return ""
	}
	return name[i+1:]
}

func main() {
	files := []string{
		"report.final.pdf",
		"backup.tar.gz",
		"README",
		"archive.",
	}
	for _, f := range files {
		fmt.Printf("%-20s -> %q\n", f, ext(f))
	}
}
Output
report.final.pdf     -> "pdf"
backup.tar.gz        -> "gz"
README               -> ""
archive.             -> ""

report.final.pdf liefert pdf (nicht final.pdf), weil nur der letzte Punkt zählt. backup.tar.gz ergibt gz — wer tar.gz als zusammenhängende Endung erkennen will, braucht eine Whitelist oder zwei LastIndex-Schritte. README und archive. geben leere Strings zurück; den Trailing-Dot-Fall fängt der i == len(name)-1-Check ab.

Ein POSIX-Pfad lässt sich am letzten / in Verzeichnis und Dateinamen aufteilen. LastIndex(path, "/") markiert die Grenze, path[i+1:] ist der Basename. Für plattformübergreifende Pfade (Windows mit Backslashes) gehört das in filepath.Base, aber für URL-Pfade und Logging-Ausgaben ist die direkte Variante schneller und transparenter.

Go basename.go
package main

import (
	"fmt"
	"strings"
)

func basename(p string) string {
	i := strings.LastIndex(p, "/")
	if i < 0 {
		return p
	}
	return p[i+1:]
}

func main() {
	urls := []string{
		"/api/v2/users/42",
		"https://example.com/static/app.css",
		"single",
		"/trailing/",
	}
	for _, u := range urls {
		fmt.Printf("%-40s -> %q\n", u, basename(u))
	}
}
Output
/api/v2/users/42                         -> "42"
https://example.com/static/app.css       -> "app.css"
single                                   -> "single"
/trailing/                               -> ""

Der Trailing-Slash-Fall liefert einen leeren Basename — bei URLs ist das oft genau das gewünschte Signal („Verzeichnis-Resource"). Wer den letzten nicht-leeren Segmenttitel braucht, kann den String vorher mit strings.TrimRight(p, "/") säubern und danach erneut LastIndex aufrufen.

Suchrichtung rechts-nach-links

LastIndex liefert den größten Byte-Offset, an dem substr vorkommt — semantisch rechtsbündige Suche.

Byte-Offset, kein Rune-Index

Das Ergebnis ist eine Byte-Position; bei Multi-Byte-Zeichen ist es nicht der Zeichen-Index, aber immer eine gültige UTF-8-Grenze.

-1 bei Nicht-Treffer

Wie Index signalisiert -1 „nicht enthalten" — gemeinsamer if i < 0-Check für beide Funktionen.

Leerer Substring liefert len(s)

LastIndex(s, "") ist len(s), nicht 0 — das ist die einzige Asymmetrie zu Index und in Schleifen ein Stolperstein.

Rabin-Karp intern

Für längere Substrings nutzt die Implementierung eine rückwärts laufende Rabin-Karp-Variante mit linearer Komplexität.

filepath.Ext/Base oft idiomatischer

Für reale Dateipfade sind filepath.Ext und filepath.Base robuster — sie behandeln Edge Cases und Plattform-Trenner korrekt.

Threadsafe

Reine Lesefunktion auf unveränderlichen Strings — beliebig parallel aufrufbar ohne Synchronisation.

UTF-8-sicher

Byte-Suche kann keinen Treffer mitten in einer Mehrbyte-Rune liefern, weil UTF-8-Sequenzen byteweise eindeutig sind.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Das strings-Paket — String-Manipulation

Zur Übersicht