feat(go): rewrite in Go

This commit is contained in:
tiennm99 committed 2026-08-31 16:28:12 +07:00
1 parent fc066c3151
commit 7ffb470db7
7 files changed
+332 -38

No files matched your search

+6 -2
View File
@@ -1,3 +1,7 @@
.idea
*.iml
out
# Built binary
/prime-generator
# Generated output
primes.txt
+56 -1
View File
@@ -1,2 +1,57 @@
# prime-generator
Generate prime numbers from 0 to 1_000_000_000
Generates every prime from 0 up to a limit with the sieve of Eratosthenes and
writes them one per line to a text file. The default limit is 1,000,000,000.
*Originally written in Java. In 2026 it was rewritten in Go; the Java version is
on the [feature/java](https://github.com/tiennm99/prime-generator/tree/feature/java)
branch.*
## Requirements
- Go 1.25 or higher
- Disk space for the output — a full run to 1,000,000,000 writes 50,847,534
primes, roughly 500 MB
- About 1 GB of RAM at the default limit, since the sieve holds one byte per
number
## Usage
```sh
go run .
```
Or build a binary first:
```sh
go build -o prime-generator .
./prime-generator
```
### Flags
| Flag | Default | Purpose |
| --- | --- | --- |
| `-n` | `1000000000` | Upper limit, inclusive, to sieve |
| `-out` | `primes.txt` | Output file path |
```sh
# Primes below 100, written somewhere else
./prime-generator -n 100 -out small.txt
```
## Tests
```sh
go test ./...
```
The sieve is checked against an independent trial-division implementation, and
against the published prime counts at powers of ten up to 1,000,000 — so a bug
in the sieve is not mirrored by the thing checking it.
## Related
[crawl-prime](https://github.com/tiennm99/crawl-prime) reaches the same output
file from the other direction: it downloads a published prime list instead of
computing one.
+3
View File
@@ -0,0 +1,3 @@
module github.com/tiennm99/prime-generator
go 1.25.0
+35
View File
@@ -0,0 +1,35 @@
package main
import (
"flag"
"fmt"
"log"
"os"
)
const (
defaultLimit = 1_000_000_000
defaultOutput = "primes.txt"
)
func main() {
limit := flag.Int("n", defaultLimit, "upper limit, inclusive, to sieve")
out := flag.String("out", defaultOutput, "output file path")
flag.Parse()
f, err := os.Create(*out)
if err != nil {
log.Fatalf("create %s: %v", *out, err)
}
defer f.Close()
count, err := writePrimes(f, sieve(*limit))
if err != nil {
log.Fatalf("write %s: %v", *out, err)
}
if err := f.Close(); err != nil {
log.Fatalf("close %s: %v", *out, err)
}
fmt.Printf("Wrote %d prime numbers up to %d to %s\n", count, *limit, *out)
}
+58
View File
@@ -0,0 +1,58 @@
package main
import (
"bufio"
"io"
"strconv"
)
// sieve marks primality for every value in [0, limit] with the sieve of
// Eratosthenes. Index i holds true when i is prime, so the slice is indexed by
// the number itself and positions 0 and 1 stay false.
//
// A limit below 2 has no primes at all; the nil slice ranges as empty.
func sieve(limit int) []bool {
if limit < 2 {
return nil
}
isPrime := make([]bool, limit+1)
for i := 2; i <= limit; i++ {
isPrime[i] = true
}
// Composites below p*p already carry a smaller factor, so crossing out can
// start there, and a p past sqrt(limit) has nothing left to mark.
for p := 2; p*p <= limit; p++ {
if isPrime[p] {
for i := p * p; i <= limit; i += p {
isPrime[i] = false
}
}
}
return isPrime
}
// writePrimes writes one prime per line, ascending, and reports how many it
// wrote. The output is buffered because a full run emits tens of millions of
// lines.
func writePrimes(w io.Writer, isPrime []bool) (int, error) {
bw := bufio.NewWriter(w)
count := 0
for n, prime := range isPrime {
if !prime {
continue
}
if _, err := bw.WriteString(strconv.Itoa(n)); err != nil {
return count, err
}
if err := bw.WriteByte('\n'); err != nil {
return count, err
}
count++
}
return count, bw.Flush()
}
+174
View File
@@ -0,0 +1,174 @@
package main
import (
"errors"
"strconv"
"strings"
"testing"
)
// primesByTrialDivision is a deliberately naive independent implementation, so
// a bug in the sieve is not mirrored by the thing checking it.
func primesByTrialDivision(limit int) []int {
var primes []int
for n := 2; n <= limit; n++ {
isPrime := true
for d := 2; d*d <= n; d++ {
if n%d == 0 {
isPrime = false
break
}
}
if isPrime {
primes = append(primes, n)
}
}
return primes
}
func primesFrom(isPrime []bool) []int {
var primes []int
for n, prime := range isPrime {
if prime {
primes = append(primes, n)
}
}
return primes
}
func TestSieveMatchesTrialDivision(t *testing.T) {
const limit = 5000
got := primesFrom(sieve(limit))
want := primesByTrialDivision(limit)
if len(got) != len(want) {
t.Fatalf("sieve(%d) produced %d primes, trial division produced %d", limit, len(got), len(want))
}
for i := range want {
if got[i] != want[i] {
t.Fatalf("sieve(%d) primes[%d] = %d, want %d", limit, i, got[i], want[i])
}
}
}
func TestSieveKnownSmallPrimes(t *testing.T) {
want := []int{2, 3, 5, 7, 11, 13, 17, 19, 23, 29}
got := primesFrom(sieve(30))
if len(got) != len(want) {
t.Fatalf("sieve(30) = %v, want %v", got, want)
}
for i := range want {
if got[i] != want[i] {
t.Errorf("sieve(30)[%d] = %d, want %d", i, got[i], want[i])
}
}
}
// The prime-counting function at powers of ten is a published, independently
// known result, so it pins the sieve at a scale trial division cannot reach.
func TestSievePrimeCounts(t *testing.T) {
cases := []struct {
limit int
want int
}{
{10, 4},
{100, 25},
{1_000, 168},
{10_000, 1_229},
{100_000, 9_592},
{1_000_000, 78_498},
}
for _, tc := range cases {
t.Run(strconv.Itoa(tc.limit), func(t *testing.T) {
if got := len(primesFrom(sieve(tc.limit))); got != tc.want {
t.Errorf("pi(%d) = %d, want %d", tc.limit, got, tc.want)
}
})
}
}
func TestSieveBelowTwoHasNoPrimes(t *testing.T) {
for _, limit := range []int{-1, 0, 1} {
if got := primesFrom(sieve(limit)); len(got) != 0 {
t.Errorf("sieve(%d) = %v, want no primes", limit, got)
}
}
}
func TestSieveExactlyTwo(t *testing.T) {
got := primesFrom(sieve(2))
if len(got) != 1 || got[0] != 2 {
t.Errorf("sieve(2) = %v, want [2]", got)
}
}
// 0 and 1 are not prime, and the slice is indexed by the number itself, so
// those positions must stay false rather than shifting every later index.
func TestSieveIndexingIsByNumber(t *testing.T) {
isPrime := sieve(10)
if len(isPrime) != 11 {
t.Fatalf("len(sieve(10)) = %d, want 11", len(isPrime))
}
if isPrime[0] || isPrime[1] {
t.Error("0 and 1 must not be marked prime")
}
if !isPrime[7] {
t.Error("7 must be marked prime")
}
if isPrime[9] {
t.Error("9 must not be marked prime")
}
}
func TestWritePrimesFormat(t *testing.T) {
var buf strings.Builder
count, err := writePrimes(&buf, sieve(20))
if err != nil {
t.Fatalf("writePrimes returned error: %v", err)
}
const want = "2\n3\n5\n7\n11\n13\n17\n19\n"
if buf.String() != want {
t.Errorf("output = %q, want %q", buf.String(), want)
}
if count != 8 {
t.Errorf("count = %d, want 8", count)
}
}
func TestWritePrimesEmpty(t *testing.T) {
var buf strings.Builder
count, err := writePrimes(&buf, sieve(1))
if err != nil {
t.Fatalf("writePrimes returned error: %v", err)
}
if buf.String() != "" {
t.Errorf("output = %q, want empty", buf.String())
}
if count != 0 {
t.Errorf("count = %d, want 0", count)
}
}
type errWriter struct{ err error }
func (w errWriter) Write([]byte) (int, error) { return 0, w.err }
func TestWritePrimesPropagatesWriteError(t *testing.T) {
wantErr := errors.New("disk full")
// Enough primes to overflow bufio's buffer, so the failure surfaces during
// writing rather than only at the final flush.
_, err := writePrimes(errWriter{err: wantErr}, sieve(200_000))
if !errors.Is(err, wantErr) {
t.Errorf("writePrimes error = %v, want %v", err, wantErr)
}
}
-35
View File
@@ -1,35 +0,0 @@
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
public class Main {
public static void main(String[] args) {
int n = 1_000_000_000; // Upper limit
boolean[] isPrime = new boolean[n + 1];
for (int i = 2; i <= n; i++) {
isPrime[i] = true;
}
for (int p = 2; p * p <= n; p++) {
if (isPrime[p]) {
for (int i = p * p; i <= n; i += p) {
isPrime[i] = false;
}
}
}
try (BufferedWriter writer = new BufferedWriter(new FileWriter("primes.txt"))) {
for (int i = 2; i <= n; i++) {
if (isPrime[i]) {
writer.write(Integer.toString(i));
writer.newLine();
}
}
System.out.println("Prime numbers up to " + n + " have been saved to primes.txt.");
} catch (IOException e) {
e.printStackTrace();
}
}
}