Conways Game of Life ist ein berühmter zellulärer Automat, den der britische Mathematiker John Horton Conway 1970 erdacht hat. Es ist ein Nullspieler-Spiel: Du legst einen Ausgangszustand fest und beobachtest, wie er sich nach festen Regeln weiterentwickelt — ohne weitere Eingaben. Trotz seiner einfachen Regeln erzeugt es überraschend komplexe, lebensähnliche Muster.
In diesem Post implementieren wir Game of Life in Go von Grund auf, Schritt für Schritt. Am Ende hast du eine funktionierende Terminal-Simulation, die einen Gleiter und ein Leichtes Raumschiff (LWSS) in Echtzeit rendert.

Voraussetzungen
Du brauchst Go auf deinem Rechner. Die Standardbibliothek reicht vollständig aus — keine Drittanbieter-Pakete benötigt.
Linux / macOS:
# Installation prüfen
go version
Windows (PowerShell):
# Installation prüfen
go version
Wenn go version etwas wie go version go1.22 linux/amd64 ausgibt, bist du startklar. Falls nicht, folge der offiziellen Installationsanleitung.
Erstelle eine neue Datei namens main.go in einem leeren Verzeichnis und folge der Anleitung.
Regeln
Das Game of Life folgt vier Regeln, die für jeden Schritt gleichzeitig auf alle Zellen des Gitters angewendet werden:
- Eine lebende Zelle mit weniger als zwei lebenden Nachbarn stirbt — Unterbevölkerung.
- Eine lebende Zelle mit zwei oder drei lebenden Nachbarn überlebt.
- Eine lebende Zelle mit mehr als drei lebenden Nachbarn stirbt — Überbevölkerung.
- Eine tote Zelle mit genau drei lebenden Nachbarn wird lebendig — Reproduktion.
Mehr braucht es nicht. Der Rest ergibt sich aus dem Ausgangszustand.
Implementierung
Wir unterteilen die Implementierung in fünf überschaubare Schritte.
1. Datenstrukturen definieren
Cell ist ein einfacher Boolean — lebendig oder tot. Grid hält die Dimensionen und ein zweidimensionales Slice von Zellen.
type Cell bool
type Grid struct {
Width int
Height int
Cells [][]Cell
}
2. Das Gitter initialisieren
NewGrid allokiert das zweidimensionale Slice und setzt die anfänglich lebenden Zellen an den angegebenen Koordinaten.
func NewGrid(width, height int, initialLiveCells [][2]int) Grid {
cells := make([][]Cell, height)
for i := range cells {
cells[i] = make([]Cell, width)
}
for _, coord := range initialLiveCells {
x, y := coord[0], coord[1]
cells[y][x] = true
}
return Grid{Width: width, Height: height, Cells: cells}
}
3. Die Spiellogik implementieren
NextState berechnet die nächste Generation. Wir allokieren ein neues Slice, damit wir nie denselben Zustand gleichzeitig lesen und schreiben — ein klassischer Fehler bei Game-of-Life-Implementierungen.
func (g *Grid) NextState() {
newCells := make([][]Cell, g.Height)
for i := range newCells {
newCells[i] = make([]Cell, g.Width)
}
for y := 0; y < g.Height; y++ {
for x := 0; x < g.Width; x++ {
liveNeighbors := g.countLiveNeighbors(x, y)
if g.Cells[y][x] && (liveNeighbors == 2 || liveNeighbors == 3) {
newCells[y][x] = true
} else if !g.Cells[y][x] && liveNeighbors == 3 {
newCells[y][x] = true
}
}
}
g.Cells = newCells
}
countLiveNeighbors prüft die acht umgebenden Zellen. Der %-Operator erzeugt ein toroidales Gitter — Zellen, die eine Kante verlassen, erscheinen auf der gegenüberliegenden Seite wieder, sodass die Simulation keine Randeffekte hat.
func (g *Grid) countLiveNeighbors(x, y int) int {
count := 0
for i := -1; i <= 1; i++ {
for j := -1; j <= 1; j++ {
if i == 0 && j == 0 {
continue
}
// Wrapping: (x + offset + Width) % Width hält den Wert in [0, Width)
x2 := (x + i + g.Width) % g.Width
y2 := (y + j + g.Height) % g.Height
if g.Cells[y2][x2] {
count++
}
}
}
return count
}
4. Das Gitter rendern
Render gibt das Gitter im Terminal aus. Lebende Zellen erscheinen als █, tote als Leerzeichen. Die ANSI-Escape-Sequenz \033[H bewegt den Cursor vor jedem Frame in die obere linke Ecke — das verhindert, dass die Ausgabe scrollt, und erzeugt einen flüssigen Animationseffekt.
func (g *Grid) Render() {
// Cursor an den Anfang bewegen, ohne zu löschen (verhindert Flackern)
fmt.Print("\033[H")
for y := 0; y < g.Height; y++ {
for x := 0; x < g.Width; x++ {
if g.Cells[y][x] {
fmt.Print("█")
} else {
fmt.Print(" ")
}
}
fmt.Println()
}
}
5. Die Haupt-Schleife aufsetzen
Wir befüllen das Gitter mit zwei bekannten Mustern — einem Gleiter (bewegt sich diagonal) und einem Leichten Raumschiff (LWSS) (bewegt sich horizontal) — und lassen die Simulation endlos laufen.
func main() {
initialLiveCells := [][2]int{
// Gleiter — bewegt sich diagonal
{1, 0}, {2, 1}, {0, 2}, {1, 2}, {2, 2},
// Leichtes Raumschiff (LWSS) — bewegt sich horizontal
{10, 2}, {11, 2}, {12, 2}, {13, 2},
{9, 3}, {13, 3},
{13, 4},
{9, 5}, {12, 5},
}
// Terminal einmalig zu Beginn leeren
fmt.Print("\033[2J")
grid := NewGrid(40, 20, initialLiveCells)
for {
grid.Render()
grid.NextState()
time.Sleep(100 * time.Millisecond)
}
}
Vollständiger Code
Speichere dies als main.go:
package main
import (
"fmt"
"time"
)
type Cell bool
type Grid struct {
Width int
Height int
Cells [][]Cell
}
func NewGrid(width, height int, initialLiveCells [][2]int) Grid {
cells := make([][]Cell, height)
for i := range cells {
cells[i] = make([]Cell, width)
}
for _, coord := range initialLiveCells {
x, y := coord[0], coord[1]
cells[y][x] = true
}
return Grid{Width: width, Height: height, Cells: cells}
}
func (g *Grid) NextState() {
newCells := make([][]Cell, g.Height)
for i := range newCells {
newCells[i] = make([]Cell, g.Width)
}
for y := 0; y < g.Height; y++ {
for x := 0; x < g.Width; x++ {
liveNeighbors := g.countLiveNeighbors(x, y)
if g.Cells[y][x] && (liveNeighbors == 2 || liveNeighbors == 3) {
newCells[y][x] = true
} else if !g.Cells[y][x] && liveNeighbors == 3 {
newCells[y][x] = true
}
}
}
g.Cells = newCells
}
func (g *Grid) countLiveNeighbors(x, y int) int {
count := 0
for i := -1; i <= 1; i++ {
for j := -1; j <= 1; j++ {
if i == 0 && j == 0 {
continue
}
x2 := (x + i + g.Width) % g.Width
y2 := (y + j + g.Height) % g.Height
if g.Cells[y2][x2] {
count++
}
}
}
return count
}
func (g *Grid) Render() {
fmt.Print("\033[H")
for y := 0; y < g.Height; y++ {
for x := 0; x < g.Width; x++ {
if g.Cells[y][x] {
fmt.Print("█")
} else {
fmt.Print(" ")
}
}
fmt.Println()
}
}
func main() {
initialLiveCells := [][2]int{
{1, 0}, {2, 1}, {0, 2}, {1, 2}, {2, 2},
{10, 2}, {11, 2}, {12, 2}, {13, 2},
{9, 3}, {13, 3},
{13, 4},
{9, 5}, {12, 5},
}
fmt.Print("\033[2J")
grid := NewGrid(40, 20, initialLiveCells)
for {
grid.Render()
grid.NextState()
time.Sleep(100 * time.Millisecond)
}
}
Das Programm starten
Linux / macOS:
go run main.go
Windows (PowerShell):
go run main.go
Mit Ctrl+C beendest du die Simulation. Du solltest den Gleiter und das LWSS-Muster quer durch das Terminal wandern sehen.
Wie es weitergeht
- Probiere andere Startmuster — die LifeWiki katalogisiert tausende davon.
- Ersetze
fmt.Printdurch eine Terminal-Bibliothek wie tcell oder bubbletea für Farben und Tastatureingaben. - Wenn dich das Containerisieren von Go-Anwendungen interessiert, schau in meinen Artikel über Go und Docker.
