1613 lines
67 KiB
HTML
1613 lines
67 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>JavaScript → Go — a phrasebook</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,300;12..96,400;12..96,500;12..96,700;12..96,800&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
|
|
<style>
|
|
:root{
|
|
/* palette drawn from the two languages' own identities */
|
|
--paper:#E7EDF1; /* cool godoc-ish paper */
|
|
--card:#FFFFFF;
|
|
--ink:#0B1215;
|
|
--ink-soft:#5A6B76;
|
|
--rule:#C9D5DD;
|
|
--go:#00ADD8; /* gopher blue */
|
|
--go-deep:#007D9C;
|
|
--js:#F7DF1E; /* js yellow */
|
|
--js-tint:#FFFDF4; /* faint wash behind js code */
|
|
--go-tint:#F4FAFC; /* faint wash behind go code */
|
|
--fuchsia:#CE3262; /* go brand fuchsia */
|
|
--teal:#00776F; /* go brand aqua, darkened for contrast */
|
|
|
|
--mono:"JetBrains Mono",ui-monospace,SFMono-Regular,Menlo,monospace;
|
|
--sans:"Bricolage Grotesque","Helvetica Neue",Arial,sans-serif;
|
|
}
|
|
|
|
*{box-sizing:border-box}
|
|
html{scroll-behavior:smooth;scroll-padding-top:88px}
|
|
body{
|
|
margin:0;
|
|
background:var(--paper);
|
|
color:var(--ink);
|
|
font-family:var(--sans);
|
|
font-size:17px;
|
|
line-height:1.5;
|
|
-webkit-font-smoothing:antialiased;
|
|
}
|
|
a{color:var(--go-deep)}
|
|
:focus-visible{outline:2.5px solid var(--fuchsia);outline-offset:3px;border-radius:2px}
|
|
|
|
.wrap{max-width:1240px;margin:0 auto;padding:0 24px}
|
|
|
|
/* ── sticky bar ─────────────────────────────── */
|
|
.bar{
|
|
position:sticky;top:0;z-index:50;
|
|
background:rgba(231,237,241,.88);
|
|
backdrop-filter:blur(10px);
|
|
border-bottom:1px solid var(--rule);
|
|
}
|
|
.bar-in{display:flex;align-items:center;gap:20px;height:64px}
|
|
.mark{
|
|
font-family:var(--mono);font-size:13px;font-weight:700;
|
|
letter-spacing:.08em;white-space:nowrap;text-decoration:none;color:var(--ink)
|
|
}
|
|
.mark em{font-style:normal;color:var(--fuchsia)}
|
|
.mark b{font-weight:700;color:var(--go-deep)}
|
|
.search{margin-left:auto;position:relative;display:flex;align-items:center}
|
|
.search input{
|
|
font-family:var(--mono);font-size:13px;
|
|
padding:9px 40px 9px 34px;width:280px;
|
|
border:1px solid var(--rule);border-radius:999px;
|
|
background:var(--card);color:var(--ink);
|
|
}
|
|
.search input::placeholder{color:#93A3AE}
|
|
.search svg{position:absolute;left:12px;width:14px;height:14px;stroke:var(--ink-soft);fill:none;stroke-width:2}
|
|
.search kbd{
|
|
position:absolute;right:12px;font-family:var(--mono);font-size:10.5px;
|
|
color:var(--ink-soft);border:1px solid var(--rule);border-radius:5px;
|
|
padding:1px 6px;background:var(--paper);pointer-events:none
|
|
}
|
|
.count{font-family:var(--mono);font-size:12px;color:var(--ink-soft);white-space:nowrap}
|
|
|
|
/* ── hero ───────────────────────────────────── */
|
|
.hero{padding:88px 0 64px;border-bottom:1px solid var(--rule)}
|
|
.eyebrow{
|
|
font-family:var(--mono);font-size:12px;letter-spacing:.16em;
|
|
text-transform:uppercase;color:var(--ink-soft);margin:0 0 26px
|
|
}
|
|
.title{
|
|
font-family:var(--sans);
|
|
font-size:clamp(3rem,10vw,7.5rem);
|
|
font-weight:800;line-height:.86;letter-spacing:-.045em;
|
|
margin:0;
|
|
}
|
|
.title .arrow{color:var(--fuchsia);display:inline-block;padding:0 .06em}
|
|
.title .go{color:var(--go-deep)}
|
|
.deck{
|
|
max-width:40ch;margin:28px 0 0;font-size:1.15rem;color:var(--ink-soft);
|
|
font-weight:400;line-height:1.45
|
|
}
|
|
|
|
/* live rotating pair */
|
|
.demo{
|
|
margin-top:46px;background:var(--card);border:1px solid var(--rule);
|
|
border-radius:14px;overflow:hidden;max-width:820px;
|
|
box-shadow:0 1px 0 rgba(11,18,21,.04),0 14px 34px -26px rgba(11,18,21,.5)
|
|
}
|
|
.demo-row{display:grid;grid-template-columns:1fr 52px 1fr;align-items:stretch}
|
|
.demo-cell{padding:26px 22px;display:flex;align-items:center;min-height:96px}
|
|
.demo-cell code{font-family:var(--mono);font-size:.92rem;line-height:1.6;white-space:pre-wrap}
|
|
.demo-mid{
|
|
display:grid;place-items:center;border-left:1px solid var(--rule);
|
|
border-right:1px solid var(--rule);color:var(--go);font-size:1.5rem;
|
|
font-family:var(--mono);background:#F6F9FB
|
|
}
|
|
.demo-cell.js{border-top:3px solid var(--js);background:var(--js-tint)}
|
|
.demo-cell.go{border-top:3px solid var(--go);background:var(--go-tint)}
|
|
.fade{transition:opacity .45s ease}
|
|
.fade.out{opacity:0}
|
|
|
|
/* contents chips — mobile / narrow only */
|
|
.contents{margin-top:52px}
|
|
.contents h2{
|
|
font-family:var(--mono);font-size:12px;letter-spacing:.16em;text-transform:uppercase;
|
|
color:var(--ink-soft);font-weight:500;margin:24px 0 10px
|
|
}
|
|
.contents h2:first-of-type{margin-top:0}
|
|
.chips{display:flex;flex-wrap:wrap;gap:8px;padding:0;margin:0;list-style:none}
|
|
.chips a{
|
|
display:inline-block;font-family:var(--mono);font-size:12.5px;
|
|
padding:6px 12px;border:1px solid var(--rule);border-radius:999px;
|
|
background:var(--card);color:var(--ink);text-decoration:none;
|
|
transition:border-color .15s,color .15s
|
|
}
|
|
.chips a:hover{border-color:var(--go);color:var(--go-deep)}
|
|
|
|
/* ── two-column reading layout ──────────────── */
|
|
.layout{display:grid;grid-template-columns:236px minmax(0,1fr);gap:56px;align-items:start}
|
|
.side-nav{
|
|
position:sticky;top:88px;max-height:calc(100vh - 112px);overflow-y:auto;
|
|
padding:32px 0 40px;font-family:var(--mono);scrollbar-width:thin
|
|
}
|
|
.side-nav h2{
|
|
font-size:10.5px;letter-spacing:.16em;text-transform:uppercase;
|
|
color:var(--ink-soft);font-weight:500;margin:26px 0 8px
|
|
}
|
|
.side-nav h2:first-child{margin-top:0}
|
|
.side-nav a{
|
|
display:block;font-size:12.5px;line-height:1.45;padding:3.5px 0 3.5px 12px;
|
|
color:var(--ink-soft);text-decoration:none;border-left:2px solid transparent;
|
|
transition:color .12s,border-color .12s
|
|
}
|
|
.side-nav a:hover{color:var(--ink)}
|
|
.side-nav a.on{color:var(--fuchsia);border-left-color:var(--fuchsia)}
|
|
.side-nav a[hidden]{display:none}
|
|
|
|
/* ── groups and entries ─────────────────────── */
|
|
.entries{padding:0 0 120px;min-width:0}
|
|
.group{padding-top:8px}
|
|
.group[hidden]{display:none}
|
|
.group-head{
|
|
display:flex;align-items:baseline;gap:14px;
|
|
margin:56px 0 0;padding:0 0 12px;border-bottom:2px solid var(--ink)
|
|
}
|
|
.group-head h2{
|
|
font-family:var(--mono);font-size:13px;font-weight:700;letter-spacing:.16em;
|
|
text-transform:uppercase;margin:0
|
|
}
|
|
.group-head .n{font-family:var(--mono);font-size:11px;color:var(--ink-soft)}
|
|
.entry{padding:52px 0;border-bottom:1px solid var(--rule)}
|
|
.entry[hidden]{display:none}
|
|
.entry-head{display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;margin-bottom:22px}
|
|
.entry-head h3{
|
|
font-size:1.5rem;font-weight:700;letter-spacing:-.02em;margin:0;line-height:1.15
|
|
}
|
|
.entry-head h3 a{color:inherit;text-decoration:none}
|
|
.entry-head h3 a:hover::after{content:" #";color:var(--go);font-weight:400}
|
|
.entry-head .tag{
|
|
font-family:var(--mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;
|
|
color:var(--ink-soft);border:1px solid var(--rule);border-radius:999px;padding:3px 9px;
|
|
background:var(--card)
|
|
}
|
|
.pair{
|
|
display:grid;grid-template-columns:1fr 56px 1fr;align-items:stretch;
|
|
background:var(--card);border:1px solid var(--rule);border-radius:14px;overflow:hidden;
|
|
box-shadow:0 1px 0 rgba(11,18,21,.03)
|
|
}
|
|
.side{padding:20px 22px 22px;min-width:0}
|
|
.side.js{border-top:3px solid var(--js);background:var(--js-tint)}
|
|
.side.go{border-top:3px solid var(--go);background:var(--go-tint)}
|
|
.side .lang{
|
|
font-family:var(--mono);font-size:10.5px;letter-spacing:.16em;text-transform:uppercase;
|
|
color:var(--ink-soft);margin:0 0 12px;display:block
|
|
}
|
|
.side pre{margin:0;overflow-x:auto}
|
|
.side code{
|
|
font-family:var(--mono);font-size:.845rem;line-height:1.65;
|
|
white-space:pre;display:block;tab-size:4
|
|
}
|
|
.mid{
|
|
display:grid;place-items:center;background:#F6F9FB;
|
|
border-left:1px solid var(--rule);border-right:1px solid var(--rule);
|
|
color:var(--go);font-family:var(--mono);font-size:1.25rem;user-select:none
|
|
}
|
|
.note{
|
|
margin:18px 0 0;max-width:74ch;color:var(--ink-soft);font-size:1.02rem;line-height:1.55
|
|
}
|
|
.note b{color:var(--ink);font-weight:500}
|
|
.note code{
|
|
font-family:var(--mono);font-size:.86em;background:#DDE6EC;
|
|
padding:1px 5px;border-radius:4px;color:var(--ink)
|
|
}
|
|
|
|
/* token colours */
|
|
.tk{color:var(--fuchsia)} /* keyword */
|
|
.ts{color:var(--teal)} /* literal */
|
|
.tf{color:var(--go-deep)} /* call */
|
|
.tc{color:#8A97A3;font-style:italic} /* comment */
|
|
|
|
/* ── tooling table ──────────────────────────── */
|
|
.tooling{padding:56px 0 0}
|
|
.tooling h3{font-size:1.5rem;font-weight:700;letter-spacing:-.02em;margin:0 0 22px}
|
|
table{
|
|
width:100%;border-collapse:collapse;background:var(--card);
|
|
border:1px solid var(--rule);border-radius:14px;overflow:hidden;
|
|
font-family:var(--mono);font-size:.85rem
|
|
}
|
|
th{
|
|
text-align:left;padding:14px 20px;font-size:10.5px;letter-spacing:.16em;
|
|
text-transform:uppercase;color:var(--ink-soft);font-weight:500;
|
|
border-bottom:1px solid var(--rule)
|
|
}
|
|
th:first-child{box-shadow:inset 0 3px 0 var(--js)}
|
|
th:last-child{box-shadow:inset 0 3px 0 var(--go)}
|
|
td{padding:12px 20px;border-bottom:1px solid #EDF2F5;vertical-align:top}
|
|
tr:last-child td{border-bottom:none}
|
|
td:first-child{width:42%;color:var(--ink-soft)}
|
|
|
|
/* ── empty state ────────────────────────────── */
|
|
.empty{
|
|
display:none;padding:80px 0;text-align:center;font-size:1.1rem;color:var(--ink-soft)
|
|
}
|
|
.empty.on{display:block}
|
|
.empty span{font-family:var(--mono);color:var(--ink)}
|
|
|
|
footer{
|
|
padding:44px 0 80px;border-top:1px solid var(--rule);
|
|
font-family:var(--mono);font-size:12px;color:var(--ink-soft);line-height:1.8
|
|
}
|
|
|
|
/* ── responsive ─────────────────────────────── */
|
|
@media (min-width:1101px){
|
|
.contents{display:none}
|
|
}
|
|
@media (max-width:1100px){
|
|
.layout{display:block}
|
|
.side-nav{display:none}
|
|
}
|
|
@media (max-width:880px){
|
|
.pair,.demo-row{grid-template-columns:1fr}
|
|
.mid,.demo-mid{
|
|
border-left:none;border-right:none;
|
|
border-top:1px solid var(--rule);border-bottom:1px solid var(--rule);
|
|
padding:8px 0
|
|
}
|
|
.mid span,.demo-mid span{display:inline-block;transform:rotate(90deg)}
|
|
.side.go,.demo-cell.go{border-top:none}
|
|
.search input{width:150px}
|
|
.search kbd{display:none}
|
|
.deck{font-size:1.05rem}
|
|
}
|
|
@media (max-width:560px){
|
|
.bar-in{height:58px;gap:12px}
|
|
.count{display:none}
|
|
.hero{padding:56px 0 44px}
|
|
}
|
|
@media (prefers-reduced-motion:reduce){
|
|
html{scroll-behavior:auto}
|
|
.fade{transition:none}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="bar">
|
|
<div class="wrap bar-in">
|
|
<a class="mark" href="#top">JS <em>→</em> <b>GO</b></a>
|
|
<span class="count" id="count"></span>
|
|
<div class="search">
|
|
<svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="M20 20l-4-4"/></svg>
|
|
<input id="filter" type="search" placeholder="find a construct" aria-label="Filter entries">
|
|
<kbd>/</kbd>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<main id="top">
|
|
<section class="hero">
|
|
<div class="wrap">
|
|
<p class="eyebrow">A phrasebook for JavaScript developers</p>
|
|
<h1 class="title">JavaScript<br><span class="arrow">→</span><span class="go">Go</span></h1>
|
|
<p class="deck">Look up something you already write every day, and read the Go sentence that means the same thing — plus a note on where the translation stops being literal.</p>
|
|
|
|
<div class="demo" aria-label="Example translations">
|
|
<div class="demo-row">
|
|
<div class="demo-cell js"><code id="demoJs" class="fade" data-lang="js"></code></div>
|
|
<div class="demo-mid"><span>→</span></div>
|
|
<div class="demo-cell go"><code id="demoGo" class="fade" data-lang="go"></code></div>
|
|
</div>
|
|
</div>
|
|
|
|
<nav class="contents" aria-label="Contents" id="contents"></nav>
|
|
</div>
|
|
</section>
|
|
|
|
<div class="wrap layout">
|
|
<nav class="side-nav" aria-label="Contents" id="sideNav"></nav>
|
|
<div class="entries" id="entries">
|
|
<section class="group" data-group="Fundamentals">
|
|
<header class="group-head"><h2>Fundamentals</h2><span class="n"></span></header>
|
|
|
|
<article class="entry" id="entry-point">
|
|
<div class="entry-head"><h3><a href="#entry-point">Getting a program to run</a></h3><span class="tag">package main</span></div>
|
|
<div class="pair">
|
|
<div class="side js"><span class="lang">JavaScript</span><pre><code data-lang="js">// hello.js — run: node hello.js
|
|
|
|
console.log("Hello, world");
|
|
|
|
// any file can be the entry point,
|
|
// and top-level code just runs</code></pre></div>
|
|
<div class="mid"><span>→</span></div>
|
|
<div class="side go"><span class="lang">Go</span><pre><code data-lang="go">// hello.go — run: go run hello.go
|
|
|
|
package main
|
|
|
|
import "fmt"
|
|
|
|
func main() {
|
|
fmt.Println("Hello, world")
|
|
}</code></pre></div>
|
|
</div>
|
|
<p class="note">Every Go file belongs to a package. Only <code>package main</code> with a <code>func main()</code> builds into a runnable program. There is no top-level code — statements live inside functions.</p>
|
|
</article>
|
|
|
|
<article class="entry" id="variables">
|
|
<div class="entry-head"><h3><a href="#variables">Declaring variables</a></h3><span class="tag">:= and var</span></div>
|
|
<div class="pair">
|
|
<div class="side js"><span class="lang">JavaScript</span><pre><code data-lang="js">let count = 1;
|
|
count = 2;
|
|
|
|
const name = "ada";
|
|
|
|
let empty; // undefined
|
|
var old = true; // avoid</code></pre></div>
|
|
<div class="mid"><span>→</span></div>
|
|
<div class="side go"><span class="lang">Go</span><pre><code data-lang="go">count := 1 // inside a function
|
|
count = 2
|
|
|
|
const name = "ada"
|
|
|
|
var empty string // "" — the zero value
|
|
var n int // 0
|
|
var ok bool // false</code></pre></div>
|
|
</div>
|
|
<p class="note"><code>:=</code> declares and infers the type in one step, but only inside a function; at package level use <code>var</code>. There is no <b>undefined</b> — every variable starts at its type's zero value. A declared local you never read is a compile error, not a warning.</p>
|
|
</article>
|
|
|
|
<article class="entry" id="types">
|
|
<div class="entry-head"><h3><a href="#types">Types and conversion</a></h3><span class="tag">strconv</span></div>
|
|
<div class="pair">
|
|
<div class="side js"><span class="lang">JavaScript</span><pre><code data-lang="js">typeof 42; // "number"
|
|
|
|
Number("42"); // 42
|
|
String(42); // "42"
|
|
parseInt("42", 10); // 42
|
|
|
|
(42).toFixed(2); // "42.00"
|
|
1 / 2; // 0.5</code></pre></div>
|
|
<div class="mid"><span>→</span></div>
|
|
<div class="side go"><span class="lang">Go</span><pre><code data-lang="go">reflect.TypeOf(42) // int
|
|
|
|
n, err := strconv.Atoi("42")
|
|
s := strconv.Itoa(42)
|
|
f, err := strconv.ParseFloat("4.2", 64)
|
|
|
|
fmt.Sprintf("%.2f", 42.0) // "42.00"
|
|
1 / 2 // 0 — integer division
|
|
float64(1) / 2 // 0.5</code></pre></div>
|
|
</div>
|
|
<p class="note">One <code>number</code> becomes <code>int</code>, <code>int64</code>, <code>float64</code> and friends. Conversion is always explicit — <code>float64(i)</code>, <code>int(f)</code> — and never happens behind your back, so mixing an <code>int</code> and a <code>float64</code> in one expression will not compile. <code>int(2.9)</code> truncates to 2. Parsing returns a value <b>and</b> an error.</p>
|
|
</article>
|
|
|
|
<article class="entry" id="equality">
|
|
<div class="entry-head"><h3><a href="#equality">Equality and truthiness</a></h3><span class="tag">no coercion</span></div>
|
|
<div class="pair">
|
|
<div class="side js"><span class="lang">JavaScript</span><pre><code data-lang="js">1 == "1"; // true
|
|
1 === "1"; // false
|
|
|
|
if (list.length) {}
|
|
if (str) {}
|
|
if (!user) {}
|
|
|
|
value ?? fallback
|
|
user?.address?.city</code></pre></div>
|
|
<div class="mid"><span>→</span></div>
|
|
<div class="side go"><span class="lang">Go</span><pre><code data-lang="go">// == is always strict.
|
|
// comparing different types won't compile.
|
|
|
|
if len(list) > 0 { }
|
|
if str != "" { }
|
|
if user == nil { }
|
|
|
|
// no ?? and no ?. — check it
|
|
if v, ok := m[k]; ok { use(v) }
|
|
if u != nil && u.Address != nil { }</code></pre></div>
|
|
</div>
|
|
<p class="note">There is no truthiness: <code>if</code> takes a <code>bool</code> and nothing else, so every check is spelled out. Structs and arrays compare with <code>==</code> field by field; slices, maps and functions can only be compared against <code>nil</code>, and <code>reflect.DeepEqual</code> or <code>slices.Equal</code> handles the rest.</p>
|
|
</article>
|
|
|
|
<article class="entry" id="control-flow">
|
|
<div class="entry-head"><h3><a href="#control-flow">Branches and loops</a></h3><span class="tag">one keyword: for</span></div>
|
|
<div class="pair">
|
|
<div class="side js"><span class="lang">JavaScript</span><pre><code data-lang="js">if (x > 1) {} else if (y) {} else {}
|
|
|
|
for (let i = 0; i < 10; i++) {}
|
|
while (ok) {}
|
|
do {} while (ok);
|
|
|
|
for (const x of xs) {}
|
|
for (const k in obj) {}
|
|
|
|
const size = x > 1 ? "big" : "small";</code></pre></div>
|
|
<div class="mid"><span>→</span></div>
|
|
<div class="side go"><span class="lang">Go</span><pre><code data-lang="go">if x > 1 { } else if y { } else { }
|
|
if v, err := f(); err != nil { } // scoped init
|
|
|
|
for i := 0; i < 10; i++ { }
|
|
for ok { } // while
|
|
for { break } // infinite
|
|
|
|
for i, x := range xs { }
|
|
for k, v := range m { }
|
|
|
|
// no ternary — write the if</code></pre></div>
|
|
</div>
|
|
<p class="note"><code>for</code> is the only loop keyword and covers all four shapes. Conditions take no parentheses, braces are never optional, and the opening brace must sit on the same line. The scoped init form — <code>if v, err := f(); err != nil</code> — keeps <code>v</code> and <code>err</code> from leaking into the surrounding scope.</p>
|
|
</article>
|
|
|
|
<article class="entry" id="switch">
|
|
<div class="entry-head"><h3><a href="#switch">switch</a></h3><span class="tag">no break needed</span></div>
|
|
<div class="pair">
|
|
<div class="side js"><span class="lang">JavaScript</span><pre><code data-lang="js">switch (x) {
|
|
case 1:
|
|
one();
|
|
break;
|
|
case 2:
|
|
case 3:
|
|
few();
|
|
break;
|
|
default:
|
|
other();
|
|
}</code></pre></div>
|
|
<div class="mid"><span>→</span></div>
|
|
<div class="side go"><span class="lang">Go</span><pre><code data-lang="go">switch x {
|
|
case 1:
|
|
one()
|
|
case 2, 3:
|
|
few()
|
|
default:
|
|
other()
|
|
}
|
|
|
|
switch { // no subject:
|
|
case x > 10: // an if/else chain
|
|
big()
|
|
}</code></pre></div>
|
|
</div>
|
|
<p class="note">Cases break on their own; falling through is opt-in with the <code>fallthrough</code> keyword. One case can list several values, a case can carry an <code>init</code> statement, and a subjectless <code>switch</code> is the idiomatic replacement for a long if/else ladder.</p>
|
|
</article>
|
|
|
|
<article class="entry" id="closures">
|
|
<div class="entry-head"><h3><a href="#closures">Closures and loop variables</a></h3><span class="tag">captured by reference</span></div>
|
|
<div class="pair">
|
|
<div class="side js"><span class="lang">JavaScript</span><pre><code data-lang="js">function counter() {
|
|
let n = 0;
|
|
return () => ++n;
|
|
}
|
|
const next = counter();
|
|
next(); // 1
|
|
next(); // 2
|
|
|
|
for (var i = 0; i < 3; i++)
|
|
setTimeout(() => log(i)); // 3 3 3
|
|
for (let i = 0; i < 3; i++)
|
|
setTimeout(() => log(i)); // 0 1 2</code></pre></div>
|
|
<div class="mid"><span>→</span></div>
|
|
<div class="side go"><span class="lang">Go</span><pre><code data-lang="go">func counter() func() int {
|
|
n := 0
|
|
return func() int { n++; return n }
|
|
}
|
|
next := counter()
|
|
next() // 1
|
|
next() // 2
|
|
|
|
for i := 0; i < 3; i++ {
|
|
go func() { fmt.Println(i) }()
|
|
}
|
|
// 0 1 2 (in some order) — since Go 1.22
|
|
// each iteration gets a fresh i, like let</code></pre></div>
|
|
</div>
|
|
<p class="note">Closures work exactly as you expect: they capture <b>variables</b>, not values. Since Go 1.22 every loop iteration gets its own copy of the loop variable, matching <code>let</code>. In older code you will still see <code>i := i</code> inside the loop body or <code>func(i int)</code> passed as an argument — that is the pre-1.22 fix for the <code>var</code>-style sharing bug.</p>
|
|
</article>
|
|
</section>
|
|
|
|
<section class="group" data-group="Data">
|
|
<header class="group-head"><h2>Data</h2><span class="n"></span></header>
|
|
|
|
<article class="entry" id="strings">
|
|
<div class="entry-head"><h3><a href="#strings">Working with strings</a></h3><span class="tag">strings, fmt</span></div>
|
|
<div class="pair">
|
|
<div class="side js"><span class="lang">JavaScript</span><pre><code data-lang="js">const who = "world";
|
|
`Hello, ${who}!`;
|
|
|
|
"abc".toUpperCase();
|
|
"a,b".split(",");
|
|
["a", "b"].join("-");
|
|
"abc".includes("b");
|
|
"abc".trim().replace("a", "z");
|
|
|
|
"héllo".length; // 5</code></pre></div>
|
|
<div class="mid"><span>→</span></div>
|
|
<div class="side go"><span class="lang">Go</span><pre><code data-lang="go">who := "world"
|
|
fmt.Sprintf("Hello, %s!", who)
|
|
|
|
strings.ToUpper("abc")
|
|
strings.Split("a,b", ",")
|
|
strings.Join([]string{"a", "b"}, "-")
|
|
strings.Contains("abc", "b")
|
|
strings.ReplaceAll(strings.TrimSpace(s), "a", "z")
|
|
|
|
len("héllo") // 6 bytes
|
|
utf8.RuneCountInString("héllo") // 5</code></pre></div>
|
|
</div>
|
|
<p class="note">Strings are immutable UTF-8 <b>bytes</b>. <code>len</code> counts bytes; ranging over a string yields runes (code points) with their byte offsets. There are no methods on strings — the verbs live in the <code>strings</code> package. Building a string in a loop wants <code>strings.Builder</code>, not <code>+=</code>.</p>
|
|
</article>
|
|
|
|
<article class="entry" id="numbers">
|
|
<div class="entry-head"><h3><a href="#numbers">Numbers and math</a></h3><span class="tag">math, math/rand/v2</span></div>
|
|
<div class="pair">
|
|
<div class="side js"><span class="lang">JavaScript</span><pre><code data-lang="js">Math.max(1, 2);
|
|
Math.abs(-4.2);
|
|
Math.floor(1.9);
|
|
Math.round(2.5);
|
|
|
|
Math.random(); // 0..1
|
|
Math.floor(Math.random() * 10);
|
|
|
|
Number.MAX_SAFE_INTEGER;
|
|
10n ** 30n; // BigInt</code></pre></div>
|
|
<div class="mid"><span>→</span></div>
|
|
<div class="side go"><span class="lang">Go</span><pre><code data-lang="go">max(1, 2) // built-in since 1.21
|
|
math.Abs(-4.2) // float64 only
|
|
math.Floor(1.9)
|
|
math.Round(2.5)
|
|
|
|
rand.Float64() // math/rand/v2
|
|
rand.IntN(10) // 0..9, no floor dance
|
|
|
|
math.MaxInt64
|
|
new(big.Int).Exp(ten, thirty, nil)</code></pre></div>
|
|
</div>
|
|
<p class="note"><code>min</code> and <code>max</code> are built-ins; nearly everything else in <code>math</code> takes and returns <code>float64</code>, so integer work means converting in and out. Integers are real integers — no <code>0.1 + 0.2</code> class of surprise until you opt into floats, but overflow silently wraps instead of going to <code>Infinity</code>. <code>math/big</code> is the BigInt.</p>
|
|
</article>
|
|
|
|
<article class="entry" id="lists">
|
|
<div class="entry-head"><h3><a href="#lists">Lists</a></h3><span class="tag">slices</span></div>
|
|
<div class="pair">
|
|
<div class="side js"><span class="lang">JavaScript</span><pre><code data-lang="js">const xs = [1, 2, 3];
|
|
|
|
xs.push(4);
|
|
xs.length;
|
|
xs[0];
|
|
xs[1] = 9;
|
|
|
|
xs.slice(1, 3);
|
|
const ys = [...xs];
|
|
xs.concat(ys);</code></pre></div>
|
|
<div class="mid"><span>→</span></div>
|
|
<div class="side go"><span class="lang">Go</span><pre><code data-lang="go">xs := []int{1, 2, 3}
|
|
|
|
xs = append(xs, 4) // reassign!
|
|
len(xs)
|
|
xs[0]
|
|
xs[1] = 9
|
|
|
|
xs[1:3] // a view, shares memory
|
|
ys := slices.Clone(xs)
|
|
append(xs, ys...)</code></pre></div>
|
|
</div>
|
|
<p class="note"><code>append</code> returns a new slice header, so you must assign the result back. Slicing does not copy: <code>xs[1:3]</code> looks into the same underlying array, and writing through it changes the original — <code>slices.Clone</code> when you need a real copy. <code>[3]int</code> is a fixed-size array and a genuinely different type from the slice <code>[]int</code>.</p>
|
|
</article>
|
|
|
|
<article class="entry" id="iteration">
|
|
<div class="entry-head"><h3><a href="#iteration">map, filter, reduce</a></h3><span class="tag">write the loop</span></div>
|
|
<div class="pair">
|
|
<div class="side js"><span class="lang">JavaScript</span><pre><code data-lang="js">xs.map(x => x * 2);
|
|
|
|
xs.filter(x => x > 1);
|
|
|
|
xs.reduce((a, b) => a + b, 0);
|
|
|
|
xs.includes(2);
|
|
xs.indexOf(2);</code></pre></div>
|
|
<div class="mid"><span>→</span></div>
|
|
<div class="side go"><span class="lang">Go</span><pre><code data-lang="go">out := make([]int, 0, len(xs))
|
|
for _, x := range xs {
|
|
out = append(out, x*2)
|
|
}
|
|
|
|
sum := 0
|
|
for _, x := range xs {
|
|
sum += x
|
|
}
|
|
|
|
slices.Contains(xs, 2)
|
|
slices.Index(xs, 2)</code></pre></div>
|
|
</div>
|
|
<p class="note">There is no built-in <code>map</code>/<code>filter</code>/<code>reduce</code>, and idiomatic Go writes the loop rather than reaching for a helper. What generics did add is the <code>slices</code> and <code>maps</code> packages, which cover searching, comparing, cloning and sorting.</p>
|
|
</article>
|
|
|
|
<article class="entry" id="sorting">
|
|
<div class="entry-head"><h3><a href="#sorting">Sorting</a></h3><span class="tag">slices.SortFunc</span></div>
|
|
<div class="pair">
|
|
<div class="side js"><span class="lang">JavaScript</span><pre><code data-lang="js">xs.sort((a, b) => a - b); // mutates
|
|
xs.toSorted(); // copies
|
|
|
|
["b", "a"].sort();
|
|
|
|
users.sort((a, b) =>
|
|
a.age - b.age ||
|
|
a.name.localeCompare(b.name));</code></pre></div>
|
|
<div class="mid"><span>→</span></div>
|
|
<div class="side go"><span class="lang">Go</span><pre><code data-lang="go">slices.Sort(xs) // mutates
|
|
ys := slices.Clone(xs) // then sort ys
|
|
slices.Sort([]string{"b", "a"})
|
|
|
|
slices.SortFunc(users, func(a, b User) int {
|
|
if c := cmp.Compare(a.Age, b.Age); c != 0 {
|
|
return c
|
|
}
|
|
return cmp.Compare(a.Name, b.Name)
|
|
})</code></pre></div>
|
|
</div>
|
|
<p class="note">The comparator returns negative, zero or positive like JS, and <code>cmp.Compare</code> writes that for you. <code>slices.Sort</code> handles anything ordered without a comparator — and unlike JS it sorts numbers as numbers, no <code>[10, 9, 1]</code> → <code>[1, 10, 9]</code> footgun. Sorting always mutates; clone first for a <code>toSorted</code>.</p>
|
|
</article>
|
|
|
|
<article class="entry" id="maps">
|
|
<div class="entry-head"><h3><a href="#maps">Key–value lookup</a></h3><span class="tag">map[K]V</span></div>
|
|
<div class="pair">
|
|
<div class="side js"><span class="lang">JavaScript</span><pre><code data-lang="js">const ages = { ada: 36 };
|
|
|
|
ages.ada;
|
|
ages["ada"];
|
|
ages.grace = 45;
|
|
delete ages.ada;
|
|
"ada" in ages;
|
|
Object.keys(ages);
|
|
|
|
const m = new Map();
|
|
m.set("k", 1);</code></pre></div>
|
|
<div class="mid"><span>→</span></div>
|
|
<div class="side go"><span class="lang">Go</span><pre><code data-lang="go">ages := map[string]int{"ada": 36}
|
|
|
|
ages["ada"]
|
|
v, ok := ages["ada"] // ok reports presence
|
|
ages["grace"] = 45
|
|
delete(ages, "ada")
|
|
len(ages)
|
|
|
|
m := make(map[string]int)
|
|
m["k"] = 1 // nil map would panic</code></pre></div>
|
|
</div>
|
|
<p class="note">Reading a missing key returns the <b>zero value</b>, not <code>undefined</code>, so <code>ages["nobody"]</code> is <code>0</code>. The comma-ok form is how you tell "absent" from "present and zero". Iteration order is deliberately randomised, and a <code>nil</code> map is fine to read but panics on write — <code>make</code> it first.</p>
|
|
</article>
|
|
|
|
<article class="entry" id="sets">
|
|
<div class="entry-head"><h3><a href="#sets">Sets</a></h3><span class="tag">map[T]struct{}</span></div>
|
|
<div class="pair">
|
|
<div class="side js"><span class="lang">JavaScript</span><pre><code data-lang="js">const seen = new Set([1, 2]);
|
|
|
|
seen.add(3);
|
|
seen.has(2); // true
|
|
seen.delete(1);
|
|
seen.size;
|
|
|
|
[...seen];
|
|
new Set(xs); // dedupe an array</code></pre></div>
|
|
<div class="mid"><span>→</span></div>
|
|
<div class="side go"><span class="lang">Go</span><pre><code data-lang="go">seen := map[int]struct{}{1: {}, 2: {}}
|
|
|
|
seen[3] = struct{}{}
|
|
_, ok := seen[2] // true
|
|
delete(seen, 1)
|
|
len(seen)
|
|
|
|
slices.Collect(maps.Keys(seen))
|
|
// dedupe: range xs, insert each key</code></pre></div>
|
|
</div>
|
|
<p class="note">There is no set type. The idiom is a map whose values are <code>struct{}</code> — the empty struct occupies zero bytes, so you pay only for keys. <code>map[T]bool</code> works too and reads more nicely (<code>if seen[x]</code>), at the cost of a byte per entry; both are everywhere in real code.</p>
|
|
</article>
|
|
|
|
<article class="entry" id="generators">
|
|
<div class="entry-head"><h3><a href="#generators">Generators and iterators</a></h3><span class="tag">range over func</span></div>
|
|
<div class="pair">
|
|
<div class="side js"><span class="lang">JavaScript</span><pre><code data-lang="js">function* nums() {
|
|
yield 1;
|
|
yield 2;
|
|
}
|
|
|
|
for (const n of nums()) {
|
|
use(n);
|
|
if (n > 1) break;
|
|
}</code></pre></div>
|
|
<div class="mid"><span>→</span></div>
|
|
<div class="side go"><span class="lang">Go</span><pre><code data-lang="go">func nums(yield func(int) bool) {
|
|
if !yield(1) {
|
|
return // consumer broke out
|
|
}
|
|
yield(2)
|
|
}
|
|
|
|
for n := range nums { // Go 1.23+
|
|
use(n)
|
|
if n > 1 { break }
|
|
}</code></pre></div>
|
|
</div>
|
|
<p class="note">Since Go 1.23 you can <code>range</code> over a function: the loop body becomes the <code>yield</code> callback, and <code>yield</code> returning false is the <code>break</code> arriving. The <code>iter</code> package names the shapes (<code>iter.Seq[T]</code>, <code>iter.Seq2[K, V]</code>), and the standard library now hands them out — <code>maps.Keys</code>, <code>slices.Values</code>, <code>strings.Lines</code>. Before 1.23 people used channels or callbacks for this; channels for iteration are now considered a smell.</p>
|
|
</article>
|
|
</section>
|
|
<section class="group" data-group="Functions and types">
|
|
<header class="group-head"><h2>Functions and types</h2><span class="n"></span></header>
|
|
|
|
<article class="entry" id="functions">
|
|
<div class="entry-head"><h3><a href="#functions">Functions</a></h3><span class="tag">multiple returns</span></div>
|
|
<div class="pair">
|
|
<div class="side js"><span class="lang">JavaScript</span><pre><code data-lang="js">function add(a, b) { return a + b; }
|
|
const add2 = (a, b) => a + b;
|
|
|
|
function greet(name = "you") {}
|
|
function sum(...nums) {}
|
|
|
|
const { a, b } = pair();
|
|
|
|
const twice = f => x => f(f(x));</code></pre></div>
|
|
<div class="mid"><span>→</span></div>
|
|
<div class="side go"><span class="lang">Go</span><pre><code data-lang="go">func add(a, b int) int { return a + b }
|
|
add2 := func(a, b int) int { return a + b }
|
|
|
|
// no default arguments — use an options
|
|
// struct or a second constructor
|
|
func sum(nums ...int) int { ... }
|
|
|
|
a, b := pair() // two return values
|
|
|
|
func twice(f func(int) int) func(int) int { ... }</code></pre></div>
|
|
</div>
|
|
<p class="note">No default or named parameters; the usual answers are a config struct or variadic option functions. Returning several values is native, and that is how errors travel. <code>defer f.Close()</code> schedules cleanup to run when the function exits, whichever way it exits.</p>
|
|
</article>
|
|
|
|
<article class="entry" id="structs">
|
|
<div class="entry-head"><h3><a href="#structs">Objects with a shape</a></h3><span class="tag">struct</span></div>
|
|
<div class="pair">
|
|
<div class="side js"><span class="lang">JavaScript</span><pre><code data-lang="js">const user = {
|
|
name: "ada",
|
|
age: 36,
|
|
};
|
|
|
|
user.name;
|
|
|
|
JSON.stringify(user);
|
|
const back = JSON.parse(text);</code></pre></div>
|
|
<div class="mid"><span>→</span></div>
|
|
<div class="side go"><span class="lang">Go</span><pre><code data-lang="go">type User struct {
|
|
Name string `json:"name"`
|
|
Age int `json:"age"`
|
|
}
|
|
|
|
u := User{Name: "ada", Age: 36}
|
|
u.Name
|
|
|
|
b, err := json.Marshal(u)
|
|
err = json.Unmarshal(text, &back)</code></pre></div>
|
|
</div>
|
|
<p class="note">The struct is the everyday object: fixed fields, fixed types, checked at compile time. Tags in backticks control JSON names. A field is visible outside its package — and to <code>encoding/json</code> — only if it starts with a <b>capital letter</b>, which is the single most common reason a struct marshals to <code>{}</code>.</p>
|
|
</article>
|
|
|
|
<article class="entry" id="pointers">
|
|
<div class="entry-head"><h3><a href="#pointers">Copies and pointers</a></h3><span class="tag">& and *</span></div>
|
|
<div class="pair">
|
|
<div class="side js"><span class="lang">JavaScript</span><pre><code data-lang="js">// the engine decides for you:
|
|
// objects by reference, primitives by value
|
|
|
|
function rename(u) { u.name = "new"; }
|
|
rename(user); // user is changed
|
|
|
|
let n = 1;
|
|
function inc(x) { x++; }
|
|
inc(n); // n is still 1</code></pre></div>
|
|
<div class="mid"><span>→</span></div>
|
|
<div class="side go"><span class="lang">Go</span><pre><code data-lang="go">// everything is passed by value.
|
|
// you choose when to pass an address.
|
|
|
|
func rename(u *User) { u.Name = "new" }
|
|
rename(&u) // u is changed
|
|
|
|
func inc(x *int) { *x++ }
|
|
inc(&n) // n is now 2
|
|
|
|
p := &u
|
|
p.Name = "ada" // auto-dereferenced</code></pre></div>
|
|
</div>
|
|
<p class="note">A struct argument is a <b>copy</b> unless you pass a pointer, which you do when the function must mutate the original or the value is large. Slices, maps and channels already hold an internal reference, so they behave the way you expect. There is no pointer arithmetic and there is a garbage collector: this is only <code>&</code> and <code>*</code>.</p>
|
|
</article>
|
|
|
|
<article class="entry" id="classes">
|
|
<div class="entry-head"><h3><a href="#classes">Classes</a></h3><span class="tag">methods and embedding</span></div>
|
|
<div class="pair">
|
|
<div class="side js"><span class="lang">JavaScript</span><pre><code data-lang="js">class Animal {
|
|
constructor(name) { this.name = name; }
|
|
speak() { return `${this.name} makes a sound`; }
|
|
}
|
|
|
|
class Dog extends Animal {
|
|
speak() { return `${this.name} barks`; }
|
|
}
|
|
|
|
new Dog("rex").speak();</code></pre></div>
|
|
<div class="mid"><span>→</span></div>
|
|
<div class="side go"><span class="lang">Go</span><pre><code data-lang="go">type Animal struct{ Name string }
|
|
|
|
func NewAnimal(name string) *Animal {
|
|
return &Animal{Name: name}
|
|
}
|
|
|
|
func (a *Animal) Speak() string {
|
|
return a.Name + " makes a sound"
|
|
}
|
|
|
|
type Dog struct{ Animal } // embedding
|
|
|
|
func (d *Dog) Speak() string {
|
|
return d.Name + " barks"
|
|
}</code></pre></div>
|
|
</div>
|
|
<p class="note">No classes, no <code>new</code> keyword for your own types, no inheritance. A method is a function with a receiver, and the receiver replaces <code>this</code> — you name it yourself, and it is a pointer whenever the method mutates. Embedding promotes the inner type's fields and methods onto the outer one: composition, not a parent. A constructor is just a function called <code>NewDog</code>.</p>
|
|
</article>
|
|
|
|
<article class="entry" id="interfaces">
|
|
<div class="entry-head"><h3><a href="#interfaces">Duck typing</a></h3><span class="tag">implicit interfaces</span></div>
|
|
<div class="pair">
|
|
<div class="side js"><span class="lang">JavaScript</span><pre><code data-lang="js">// anything with .speak() will do,
|
|
// and you find out at runtime
|
|
|
|
function speakAll(items) {
|
|
for (const i of items) {
|
|
console.log(i.speak());
|
|
}
|
|
}
|
|
|
|
if (x instanceof Dog) {}</code></pre></div>
|
|
<div class="mid"><span>→</span></div>
|
|
<div class="side go"><span class="lang">Go</span><pre><code data-lang="go">type Speaker interface {
|
|
Speak() string
|
|
}
|
|
|
|
func speakAll(items []Speaker) {
|
|
for _, i := range items {
|
|
fmt.Println(i.Speak())
|
|
}
|
|
}
|
|
|
|
if d, ok := x.(*Dog); ok { use(d) }</code></pre></div>
|
|
</div>
|
|
<p class="note">There is no <code>implements</code>. A type satisfies an interface simply by having the methods, which means you can write an interface for someone else's type after the fact. Keep them small — <code>io.Reader</code> is one method. <code>any</code> holds anything, and you get the concrete type back with an assertion or a <code>switch v := x.(type)</code>.</p>
|
|
</article>
|
|
|
|
<article class="entry" id="generics">
|
|
<div class="entry-head"><h3><a href="#generics">Generics</a></h3><span class="tag">type parameters</span></div>
|
|
<div class="pair">
|
|
<div class="side js"><span class="lang">JavaScript</span><pre><code data-lang="js">// TypeScript
|
|
function first<T>(xs: T[]): T {
|
|
return xs[0];
|
|
}
|
|
first([1, 2]); // T inferred
|
|
|
|
interface Box<T> { value: T }
|
|
|
|
function largest<T extends Ordered>(
|
|
a: T, b: T): T {}</code></pre></div>
|
|
<div class="mid"><span>→</span></div>
|
|
<div class="side go"><span class="lang">Go</span><pre><code data-lang="go">func First[T any](xs []T) T {
|
|
return xs[0]
|
|
}
|
|
First([]int{1, 2}) // T inferred
|
|
|
|
type Box[T any] struct{ Value T }
|
|
|
|
func Largest[T cmp.Ordered](a, b T) T {
|
|
if a > b { return a }
|
|
return b
|
|
}</code></pre></div>
|
|
</div>
|
|
<p class="note">Square brackets instead of angle brackets, and a <b>constraint</b> where TS says <code>extends</code>: <code>any</code> accepts everything, <code>comparable</code> allows <code>==</code>, <code>cmp.Ordered</code> allows <code><</code>, and a union like <code>int | float64</code> writes your own. Methods cannot add their own type parameters, and the culture still reaches for a concrete type first — generics are for containers and algorithms, not for everything.</p>
|
|
</article>
|
|
|
|
<article class="entry" id="enums">
|
|
<div class="entry-head"><h3><a href="#enums">Enums</a></h3><span class="tag">const and iota</span></div>
|
|
<div class="pair">
|
|
<div class="side js"><span class="lang">JavaScript</span><pre><code data-lang="js">// TypeScript
|
|
enum Color { Red, Green, Blue }
|
|
|
|
// or the object-as-enum idiom
|
|
const Color = {
|
|
Red: "red",
|
|
Green: "green",
|
|
} as const;
|
|
|
|
let c: Color = Color.Red;</code></pre></div>
|
|
<div class="mid"><span>→</span></div>
|
|
<div class="side go"><span class="lang">Go</span><pre><code data-lang="go">type Color int
|
|
|
|
const (
|
|
Red Color = iota // 0
|
|
Green // 1
|
|
Blue // 2
|
|
)
|
|
|
|
func (c Color) String() string {
|
|
return [...]string{"red", "green", "blue"}[c]
|
|
}
|
|
|
|
var c Color = Red</code></pre></div>
|
|
</div>
|
|
<p class="note">There is no <code>enum</code> keyword: the idiom is a named type plus a <code>const</code> block, where <code>iota</code> counts up from zero one line at a time. A <code>String()</code> method makes it print nicely (the <code>stringer</code> tool generates these). Nothing checks exhaustiveness in a <code>switch</code> — linters do — and nothing stops <code>Color(42)</code>, so validate at the edges.</p>
|
|
</article>
|
|
|
|
<article class="entry" id="nil">
|
|
<div class="entry-head"><h3><a href="#nil">null, undefined and nothing</a></h3><span class="tag">one nil, many zeros</span></div>
|
|
<div class="pair">
|
|
<div class="side js"><span class="lang">JavaScript</span><pre><code data-lang="js">let a; // undefined
|
|
let b = null; // deliberate nothing
|
|
|
|
user.middleName ?? "";
|
|
|
|
function f(opt) {
|
|
if (opt === undefined) opt = {};
|
|
}</code></pre></div>
|
|
<div class="mid"><span>→</span></div>
|
|
<div class="side go"><span class="lang">Go</span><pre><code data-lang="go">var s string // "" — just the zero value
|
|
var p *User // nil — no User yet
|
|
|
|
if p == nil { }
|
|
|
|
// an "optional" field is a pointer
|
|
type Profile struct {
|
|
MiddleName *string `json:"middleName"`
|
|
}
|
|
if pr.MiddleName != nil { use(*pr.MiddleName) }</code></pre></div>
|
|
</div>
|
|
<p class="note">Go has one <code>nil</code>, not two flavours of nothing, and most of the time you do not even need it: the zero value (<code>""</code>, <code>0</code>, <code>false</code>) is designed to be useful, so "absent" and "empty" are usually allowed to mean the same thing. When the difference truly matters — a JSON field that may be omitted — the idiom is a pointer, and <code>nil</code>-checking it is your <code>?.</code>.</p>
|
|
</article>
|
|
</section>
|
|
|
|
<section class="group" data-group="When things go wrong">
|
|
<header class="group-head"><h2>When things go wrong</h2><span class="n"></span></header>
|
|
|
|
<article class="entry" id="errors">
|
|
<div class="entry-head"><h3><a href="#errors">Things going wrong</a></h3><span class="tag">errors are values</span></div>
|
|
<div class="pair">
|
|
<div class="side js"><span class="lang">JavaScript</span><pre><code data-lang="js">try {
|
|
const data = risky();
|
|
use(data);
|
|
} catch (err) {
|
|
console.error(err);
|
|
throw new Error("loading failed");
|
|
} finally {
|
|
cleanup();
|
|
}</code></pre></div>
|
|
<div class="mid"><span>→</span></div>
|
|
<div class="side go"><span class="lang">Go</span><pre><code data-lang="go">defer cleanup()
|
|
|
|
data, err := risky()
|
|
if err != nil {
|
|
return fmt.Errorf("loading: %w", err)
|
|
}
|
|
use(data)
|
|
|
|
panic("unrecoverable") // rare, not control flow</code></pre></div>
|
|
</div>
|
|
<p class="note">Nothing is thrown. An error is an ordinary value returned next to the result, and the <code>if err != nil</code> block you write every few lines <b>is</b> the idiom, not a sign you are doing it wrong. <code>%w</code> wraps an error with context — the equivalent of re-throwing with a <code>cause</code> — so callers can still see what is underneath.</p>
|
|
</article>
|
|
|
|
<article class="entry" id="custom-errors">
|
|
<div class="entry-head"><h3><a href="#custom-errors">Making your own errors</a></h3><span class="tag">errors.Is, errors.As</span></div>
|
|
<div class="pair">
|
|
<div class="side js"><span class="lang">JavaScript</span><pre><code data-lang="js">class NotFoundError extends Error {}
|
|
|
|
class ValidationError extends Error {
|
|
constructor(field) {
|
|
super(`invalid ${field}`);
|
|
this.field = field;
|
|
}
|
|
}
|
|
|
|
if (err instanceof NotFoundError) {}
|
|
if (err instanceof ValidationError) {
|
|
use(err.field);
|
|
}
|
|
err.cause;</code></pre></div>
|
|
<div class="mid"><span>→</span></div>
|
|
<div class="side go"><span class="lang">Go</span><pre><code data-lang="go">var ErrNotFound = errors.New("not found")
|
|
|
|
type ValidationError struct{ Field string }
|
|
|
|
func (e *ValidationError) Error() string {
|
|
return "invalid " + e.Field
|
|
}
|
|
|
|
if errors.Is(err, ErrNotFound) { }
|
|
|
|
var ve *ValidationError
|
|
if errors.As(err, &ve) {
|
|
use(ve.Field)
|
|
}</code></pre></div>
|
|
</div>
|
|
<p class="note">Anything with an <code>Error() string</code> method is an error. Two idioms replace subclassing: a package-level <b>sentinel</b> (<code>ErrNotFound</code>) when callers only need identity, and a <b>struct type</b> when the error carries data. <code>errors.Is</code> asks "is this that sentinel, anywhere in the wrap chain?"; <code>errors.As</code> is <code>instanceof</code> plus the cast, fishing the typed error out of the chain.</p>
|
|
</article>
|
|
</section>
|
|
|
|
<section class="group" data-group="Concurrency">
|
|
<header class="group-head"><h2>Concurrency</h2><span class="n"></span></header>
|
|
|
|
<article class="entry" id="async">
|
|
<div class="entry-head"><h3><a href="#async">Doing things at the same time</a></h3><span class="tag">goroutines</span></div>
|
|
<div class="pair">
|
|
<div class="side js"><span class="lang">JavaScript</span><pre><code data-lang="js">async function load() {
|
|
const a = await fetchA();
|
|
return a;
|
|
}
|
|
|
|
const [x, y] = await Promise.all([p1(), p2()]);
|
|
|
|
setTimeout(fn, 1000);</code></pre></div>
|
|
<div class="mid"><span>→</span></div>
|
|
<div class="side go"><span class="lang">Go</span><pre><code data-lang="go">ch := make(chan string)
|
|
go func() { ch <- fetchA() }()
|
|
a := <-ch // blocks until ready
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(2)
|
|
go func() { defer wg.Done(); p1() }()
|
|
go func() { defer wg.Done(); p2() }()
|
|
wg.Wait()
|
|
|
|
time.AfterFunc(time.Second, fn)</code></pre></div>
|
|
</div>
|
|
<p class="note">No event loop, and no coloured functions: any ordinary function becomes concurrent by putting <code>go</code> in front of the call. Channels move values between goroutines, and an unbuffered send blocks until someone receives — that blocking is the synchronisation. Nothing waits for your goroutines when <code>main</code> returns.</p>
|
|
</article>
|
|
|
|
<article class="entry" id="cancellation">
|
|
<div class="entry-head"><h3><a href="#cancellation">Racing and cancelling</a></h3><span class="tag">select, context</span></div>
|
|
<div class="pair">
|
|
<div class="side js"><span class="lang">JavaScript</span><pre><code data-lang="js">await Promise.race([work(), timeout(1000)]);
|
|
|
|
const c = new AbortController();
|
|
fetch(url, { signal: c.signal });
|
|
c.abort();</code></pre></div>
|
|
<div class="mid"><span>→</span></div>
|
|
<div class="side go"><span class="lang">Go</span><pre><code data-lang="go">select {
|
|
case v := <-work:
|
|
use(v)
|
|
case <-time.After(time.Second):
|
|
return errTimeout
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(ctx, time.Second)
|
|
defer cancel()
|
|
req = req.WithContext(ctx)</code></pre></div>
|
|
</div>
|
|
<p class="note"><code>select</code> waits on several channel operations and proceeds with whichever is ready first, so it covers both <code>Promise.race</code> and a non-blocking poll (with a <code>default</code> case). <code>context</code> carries deadlines and cancellation down a call chain and is, by convention, the first parameter of anything that blocks or touches the network.</p>
|
|
</article>
|
|
|
|
<article class="entry" id="shared-state">
|
|
<div class="entry-head"><h3><a href="#shared-state">Sharing state safely</a></h3><span class="tag">sync, atomic</span></div>
|
|
<div class="pair">
|
|
<div class="side js"><span class="lang">JavaScript</span><pre><code data-lang="js">// one thread: this is always safe
|
|
let count = 0;
|
|
async function hit() { count++; }
|
|
|
|
// only workers need real machinery:
|
|
// SharedArrayBuffer + Atomics
|
|
Atomics.add(shared, 0, 1);</code></pre></div>
|
|
<div class="mid"><span>→</span></div>
|
|
<div class="side go"><span class="lang">Go</span><pre><code data-lang="go">// goroutines share memory: this is a race
|
|
var mu sync.Mutex
|
|
mu.Lock()
|
|
count++
|
|
mu.Unlock()
|
|
|
|
var n atomic.Int64
|
|
n.Add(1)
|
|
|
|
var once sync.Once
|
|
once.Do(connect) // lazy init, exactly once</code></pre></div>
|
|
</div>
|
|
<p class="note">The single-threaded safety you are used to is gone: two goroutines touching the same variable without coordination is a <b>data race</b> and genuinely undefined behaviour. <code>go test -race</code> catches these and belongs in CI from day one. The proverb: share memory by communicating (channels) rather than communicating by sharing memory (mutexes) — but a plain <code>sync.Mutex</code> guarding a struct is honest, common Go.</p>
|
|
</article>
|
|
</section>
|
|
<section class="group" data-group="Everyday standard library">
|
|
<header class="group-head"><h2>Everyday standard library</h2><span class="n"></span></header>
|
|
|
|
<article class="entry" id="time">
|
|
<div class="entry-head"><h3><a href="#time">Dates, times and timers</a></h3><span class="tag">time</span></div>
|
|
<div class="pair">
|
|
<div class="side js"><span class="lang">JavaScript</span><pre><code data-lang="js">Date.now();
|
|
new Date().toISOString();
|
|
|
|
const d = new Date("2026-07-31");
|
|
d.getFullYear();
|
|
|
|
// formatting: Intl or a library
|
|
new Intl.DateTimeFormat("en").format(d);
|
|
|
|
setTimeout(fn, 1000);
|
|
const id = setInterval(fn, 1000);
|
|
clearInterval(id);</code></pre></div>
|
|
<div class="mid"><span>→</span></div>
|
|
<div class="side go"><span class="lang">Go</span><pre><code data-lang="go">time.Now()
|
|
time.Now().Format(time.RFC3339)
|
|
|
|
t, err := time.Parse("2006-01-02", "2026-07-31")
|
|
t.Year()
|
|
|
|
t.Format("Jan 2, 2006 at 3:04pm")
|
|
time.Since(start) // a Duration
|
|
t.Add(24 * time.Hour)
|
|
|
|
time.AfterFunc(time.Second, fn)
|
|
tick := time.NewTicker(time.Second)
|
|
for range tick.C { fn() } // tick.Stop() to end</code></pre></div>
|
|
</div>
|
|
<p class="note">Go's format strings are not <code>YYYY-MM-DD</code>. You write the <b>reference moment</b> — Mon Jan 2 15:04:05 MST 2006 — laid out however you want the output to look, and the same layout string drives both <code>Format</code> and <code>Parse</code>. Durations are a real type (<code>2 * time.Second</code>), so there is no guessing whether an API wants seconds or milliseconds.</p>
|
|
</article>
|
|
|
|
<article class="entry" id="regex">
|
|
<div class="entry-head"><h3><a href="#regex">Regular expressions</a></h3><span class="tag">regexp, RE2</span></div>
|
|
<div class="pair">
|
|
<div class="side js"><span class="lang">JavaScript</span><pre><code data-lang="js">const re = /a(b+)c/;
|
|
|
|
re.test("abbc"); // true
|
|
"abbc".match(re)[1]; // "bb"
|
|
"abc abc".replace(/b/g, "x");
|
|
|
|
[..."a1b2".matchAll(/\d/g)];</code></pre></div>
|
|
<div class="mid"><span>→</span></div>
|
|
<div class="side go"><span class="lang">Go</span><pre><code data-lang="go">re := regexp.MustCompile(`a(b+)c`)
|
|
|
|
re.MatchString("abbc") // true
|
|
re.FindStringSubmatch("abbc")[1] // "bb"
|
|
re.ReplaceAllString("abc abc", "x")
|
|
// replace is global by default
|
|
|
|
re.FindAllString("a1b2", -1)</code></pre></div>
|
|
</div>
|
|
<p class="note">Compile once — usually at package level with <code>MustCompile</code> — and use the raw-string backticks so <code>\d</code> stays <code>\d</code>. The engine is <b>RE2</b>: guaranteed linear time, immune to catastrophic backtracking, but in exchange there is no lookahead, no lookbehind, and no backreferences. When you miss those, restructure the pattern or do a second pass in code.</p>
|
|
</article>
|
|
|
|
<article class="entry" id="bytes">
|
|
<div class="entry-head"><h3><a href="#bytes">Bytes and buffers</a></h3><span class="tag">[]byte, io</span></div>
|
|
<div class="pair">
|
|
<div class="side js"><span class="lang">JavaScript</span><pre><code data-lang="js">const buf = Buffer.from("hi");
|
|
buf.toString();
|
|
buf.toString("hex");
|
|
buf.toString("base64");
|
|
|
|
new TextEncoder().encode("hi");
|
|
new Uint8Array(4);
|
|
|
|
// streams: ReadableStream, pipe()</code></pre></div>
|
|
<div class="mid"><span>→</span></div>
|
|
<div class="side go"><span class="lang">Go</span><pre><code data-lang="go">b := []byte("hi")
|
|
string(b)
|
|
hex.EncodeToString(b)
|
|
base64.StdEncoding.EncodeToString(b)
|
|
|
|
var buf bytes.Buffer
|
|
buf.WriteString("hi")
|
|
buf.Bytes()
|
|
|
|
// streams: io.Reader, io.Writer, io.Copy
|
|
io.Copy(dst, src)</code></pre></div>
|
|
</div>
|
|
<p class="note"><code>[]byte</code> is the Buffer, and converting to or from <code>string</code> copies (strings are immutable). The two-method universe of <code>io.Reader</code> and <code>io.Writer</code> is Go's stream story: files, sockets, HTTP bodies, gzip, hashes all speak it, and <code>io.Copy</code> is <code>pipe()</code>. The <code>bytes</code> package mirrors <code>strings</code> function for function.</p>
|
|
</article>
|
|
|
|
<article class="entry" id="files">
|
|
<div class="entry-head"><h3><a href="#files">Reading and writing files</a></h3><span class="tag">os, bufio</span></div>
|
|
<div class="pair">
|
|
<div class="side js"><span class="lang">JavaScript</span><pre><code data-lang="js">import { readFile, writeFile }
|
|
from "node:fs/promises";
|
|
|
|
const text = await readFile("a.txt", "utf8");
|
|
await writeFile("out.txt", text);
|
|
|
|
// line by line
|
|
for await (const line of rl) {}
|
|
|
|
path.join(dir, "a.txt");</code></pre></div>
|
|
<div class="mid"><span>→</span></div>
|
|
<div class="side go"><span class="lang">Go</span><pre><code data-lang="go">data, err := os.ReadFile("a.txt")
|
|
text := string(data)
|
|
err = os.WriteFile("out.txt", data, 0o644)
|
|
|
|
f, err := os.Open("a.txt")
|
|
defer f.Close()
|
|
sc := bufio.NewScanner(f)
|
|
for sc.Scan() {
|
|
line := sc.Text()
|
|
}
|
|
|
|
filepath.Join(dir, "a.txt")</code></pre></div>
|
|
</div>
|
|
<p class="note">No async variant exists because none is needed: calls block the goroutine, not the program, so "concurrent file IO" is just a goroutine. <code>os.ReadFile</code> hands you bytes, and <code>0o644</code> is the permission bits JS never asked you about. The <code>Open</code> / <code>defer Close</code> / <code>bufio.Scanner</code> trio is the streaming read for files too big to slurp.</p>
|
|
</article>
|
|
|
|
<article class="entry" id="http-client">
|
|
<div class="entry-head"><h3><a href="#http-client">Calling an API</a></h3><span class="tag">net/http client</span></div>
|
|
<div class="pair">
|
|
<div class="side js"><span class="lang">JavaScript</span><pre><code data-lang="js">const res = await fetch(url);
|
|
if (!res.ok) throw new Error(res.status);
|
|
const data = await res.json();
|
|
|
|
await fetch(url, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(payload),
|
|
});</code></pre></div>
|
|
<div class="mid"><span>→</span></div>
|
|
<div class="side go"><span class="lang">Go</span><pre><code data-lang="go">resp, err := http.Get(url)
|
|
if err != nil { return err } // transport only
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK { }
|
|
|
|
var data Result
|
|
json.NewDecoder(resp.Body).Decode(&data)
|
|
|
|
req, _ := http.NewRequestWithContext(ctx,
|
|
"POST", url, bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, err = http.DefaultClient.Do(req)</code></pre></div>
|
|
</div>
|
|
<p class="note">Same split as <code>fetch</code>: the error covers transport failure only, and a 404 is a perfectly successful response you must check yourself. Two habits matter: always <code>defer resp.Body.Close()</code> (leaked bodies leak connections), and never ship <code>http.DefaultClient</code> untouched for anything serious — it has <b>no timeout</b>, so pass a context or build a <code>Client</code> with one.</p>
|
|
</article>
|
|
|
|
<article class="entry" id="http-server">
|
|
<div class="entry-head"><h3><a href="#http-server">Serving HTTP</a></h3><span class="tag">net/http server</span></div>
|
|
<div class="pair">
|
|
<div class="side js"><span class="lang">JavaScript</span><pre><code data-lang="js">import express from "express";
|
|
const app = express();
|
|
|
|
app.get("/users/:id", (req, res) => {
|
|
res.json(find(req.params.id));
|
|
});
|
|
|
|
app.use(logger); // middleware
|
|
|
|
app.listen(3000);</code></pre></div>
|
|
<div class="mid"><span>→</span></div>
|
|
<div class="side go"><span class="lang">Go</span><pre><code data-lang="go">mux := http.NewServeMux()
|
|
|
|
mux.HandleFunc("GET /users/{id}",
|
|
func(w http.ResponseWriter, r *http.Request) {
|
|
id := r.PathValue("id")
|
|
json.NewEncoder(w).Encode(find(id))
|
|
})
|
|
|
|
handler := logger(mux) // middleware wraps
|
|
|
|
http.ListenAndServe(":3000", handler)</code></pre></div>
|
|
</div>
|
|
<p class="note">The standard library server is what you deploy — no Express equivalent required. Since Go 1.22 the router understands methods and <code>{id}</code> wildcards, read back with <code>r.PathValue</code>. Every request already runs in its own goroutine, so handlers are concurrent for free (and shared state needs a mutex). Middleware is nothing special: a function that takes an <code>http.Handler</code> and returns one.</p>
|
|
</article>
|
|
|
|
<article class="entry" id="env">
|
|
<div class="entry-head"><h3><a href="#env">Environment and flags</a></h3><span class="tag">os, flag</span></div>
|
|
<div class="pair">
|
|
<div class="side js"><span class="lang">JavaScript</span><pre><code data-lang="js">process.env.PORT ?? "3000";
|
|
|
|
process.argv.slice(2);
|
|
|
|
// flags: commander, yargs
|
|
program.option("-v, --verbose");
|
|
program.parse();</code></pre></div>
|
|
<div class="mid"><span>→</span></div>
|
|
<div class="side go"><span class="lang">Go</span><pre><code data-lang="go">port := os.Getenv("PORT") // "" if unset
|
|
port, ok := os.LookupEnv("PORT") // ok says which
|
|
|
|
os.Args[1:]
|
|
|
|
verbose := flag.Bool("v", false, "verbose output")
|
|
flag.Parse()
|
|
if *verbose { }
|
|
flag.Args() // what's left over</code></pre></div>
|
|
</div>
|
|
<p class="note"><code>LookupEnv</code> is the one to reach for when "unset" and "set to empty" differ. The <code>flag</code> package is a built-in commander: declare flags, call <code>Parse</code>, and note the pointer — the value lives behind <code>*verbose</code>. It only does <code>-flag</code> style; when you outgrow it into subcommands, <code>spf13/cobra</code> is what most CLIs use.</p>
|
|
</article>
|
|
|
|
<article class="entry" id="logging">
|
|
<div class="entry-head"><h3><a href="#logging">Logging</a></h3><span class="tag">log/slog</span></div>
|
|
<div class="pair">
|
|
<div class="side js"><span class="lang">JavaScript</span><pre><code data-lang="js">console.log("started");
|
|
console.warn("low disk");
|
|
console.error("save failed", err);
|
|
|
|
// structured: pino, winston
|
|
log.info({ id: u.id, plan: "pro" },
|
|
"user created");</code></pre></div>
|
|
<div class="mid"><span>→</span></div>
|
|
<div class="side go"><span class="lang">Go</span><pre><code data-lang="go">fmt.Println("started") // scratch output
|
|
log.Printf("low disk: %d%%", pct)
|
|
|
|
slog.Info("user created",
|
|
"id", u.ID, "plan", "pro")
|
|
slog.Error("save failed", "err", err)
|
|
|
|
slog.SetDefault(slog.New(
|
|
slog.NewJSONHandler(os.Stdout, nil)))</code></pre></div>
|
|
</div>
|
|
<p class="note">Three tiers: <code>fmt.Println</code> while poking around, <code>log</code> for timestamps and <code>Fatal</code>, and <code>slog</code> — in the standard library since 1.21 — when you want what pino gives you: leveled, structured, key-value logging with a JSON handler for production and a text handler for your terminal. Attach request-scoped fields once with <code>logger.With("reqID", id)</code>.</p>
|
|
</article>
|
|
</section>
|
|
|
|
<section class="group" data-group="Workflow">
|
|
<header class="group-head"><h2>Workflow</h2><span class="n"></span></header>
|
|
|
|
<article class="entry" id="modules">
|
|
<div class="entry-head"><h3><a href="#modules">Modules and visibility</a></h3><span class="tag">go.mod</span></div>
|
|
<div class="pair">
|
|
<div class="side js"><span class="lang">JavaScript</span><pre><code data-lang="js">// greet.js
|
|
export function hello() {}
|
|
export default thing;
|
|
|
|
// app.js
|
|
import { hello } from "./greet.js";
|
|
import thing from "some-pkg";
|
|
|
|
// package.json, node_modules/</code></pre></div>
|
|
<div class="mid"><span>→</span></div>
|
|
<div class="side go"><span class="lang">Go</span><pre><code data-lang="go">// greet/greet.go
|
|
package greet
|
|
|
|
func Hello() {} // exported: capital H
|
|
func local() {} // package-private
|
|
|
|
// main.go
|
|
import (
|
|
"fmt"
|
|
"example.com/app/greet"
|
|
)
|
|
|
|
// go.mod, shared module cache</code></pre></div>
|
|
</div>
|
|
<p class="note">Visibility is spelled with a capital letter rather than an <code>export</code> keyword, so renaming <code>hello</code> to <code>Hello</code> publishes it. A directory is a package and its import path is the module path plus that directory. Unused imports fail the build, which is why everyone runs <code>goimports</code> on save.</p>
|
|
</article>
|
|
|
|
<article class="entry" id="tests">
|
|
<div class="entry-head"><h3><a href="#tests">Tests</a></h3><span class="tag">go test</span></div>
|
|
<div class="pair">
|
|
<div class="side js"><span class="lang">JavaScript</span><pre><code data-lang="js">// add.test.js
|
|
import { test, expect } from "vitest";
|
|
|
|
test("adds", () => {
|
|
expect(add(1, 2)).toBe(3);
|
|
});
|
|
|
|
// npx vitest</code></pre></div>
|
|
<div class="mid"><span>→</span></div>
|
|
<div class="side go"><span class="lang">Go</span><pre><code data-lang="go">// add_test.go
|
|
package math
|
|
|
|
import "testing"
|
|
|
|
func TestAdd(t *testing.T) {
|
|
if got := add(1, 2); got != 3 {
|
|
t.Errorf("add(1, 2) = %d, want 3", got)
|
|
}
|
|
}
|
|
|
|
// go test ./...</code></pre></div>
|
|
</div>
|
|
<p class="note">The runner ships with the toolchain: no framework, no config, no dependency. Files end in <code>_test.go</code>, functions start with <code>Test</code> and take <code>*testing.T</code>. There are no assertion helpers on purpose — you compare values and call <code>t.Errorf</code> with a message that says what you got and what you wanted. Table-driven tests (a slice of cases, one loop) are the house style.</p>
|
|
</article>
|
|
|
|
<section class="tooling" id="tooling">
|
|
<h3>The rest of the toolbox</h3>
|
|
<table>
|
|
<thead><tr><th>JavaScript</th><th>Go</th></tr></thead>
|
|
<tbody>
|
|
<tr><td>npm install</td><td>go get · go mod tidy</td></tr>
|
|
<tr><td>package.json</td><td>go.mod</td></tr>
|
|
<tr><td>package-lock.json</td><td>go.sum</td></tr>
|
|
<tr><td>node_modules/</td><td>a shared module cache</td></tr>
|
|
<tr><td>npm run build</td><td>go build ./...</td></tr>
|
|
<tr><td>node app.js</td><td>go run .</td></tr>
|
|
<tr><td>prettier</td><td>gofmt — built in, and not configurable</td></tr>
|
|
<tr><td>eslint</td><td>go vet · staticcheck</td></tr>
|
|
<tr><td>jest · vitest</td><td>go test ./...</td></tr>
|
|
<tr><td>istanbul / c8</td><td>go test -cover</td></tr>
|
|
<tr><td>tsc</td><td>the compiler — types aren't optional</td></tr>
|
|
<tr><td>nodemon</td><td>air, or just rebuild; it's fast</td></tr>
|
|
<tr><td>npx</td><td>go run example.com/tool@latest</td></tr>
|
|
<tr><td>JSDoc</td><td>go doc · pkg.go.dev</td></tr>
|
|
<tr><td>Chrome DevTools profiler</td><td>pprof — built into the toolchain</td></tr>
|
|
<tr><td>esbuild + a Dockerfile</td><td>go build — one static binary</td></tr>
|
|
<tr><td>cross-compiling: don't</td><td>GOOS=linux GOARCH=arm64 go build</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</section>
|
|
</section>
|
|
|
|
<p class="empty" id="empty">Nothing matches <span id="term"></span>. Try <span>append</span>, <span>goroutine</span> or <span>nil</span>.</p>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<footer class="wrap">
|
|
Written against Go 1.24 · tabs are gofmt's doing, not mine<br>
|
|
Everything here compiles the way it reads, except where the note says otherwise.
|
|
</footer>
|
|
<script>
|
|
(function () {
|
|
"use strict";
|
|
|
|
/* ── minimal two-language highlighter ─────────────────────── */
|
|
var KW = {
|
|
js: /^(const|let|var|function|return|if|else|for|of|in|while|do|switch|case|default|break|continue|new|class|extends|this|try|catch|finally|throw|async|await|import|export|from|typeof|instanceof|delete|yield|static|enum|interface|as|null|undefined|true|false)$/,
|
|
go: /^(package|import|func|var|const|type|struct|interface|map|chan|go|defer|range|select|fallthrough|return|if|else|for|switch|case|default|break|continue|goto|nil|true|false|iota|make|new|len|cap|append|copy|delete|panic|recover|min|max|clear|string|int|int64|int32|uint|float64|float32|bool|byte|rune|any|comparable|error)$/
|
|
};
|
|
var RE = /(\/\/[^\n]*|\/\*[\s\S]*?\*\/)|(`[^`]*`|"(?:\\.|[^"\\\n])*"|'(?:\\.|[^'\\\n])*')|(\b\d+(?:\.\d+)?\b)|([A-Za-z_$][\w$]*)/g;
|
|
|
|
function esc(s) {
|
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
}
|
|
|
|
function highlight(src, lang) {
|
|
var kw = KW[lang] || KW.js, out = "", last = 0, m;
|
|
RE.lastIndex = 0;
|
|
while ((m = RE.exec(src)) !== null) {
|
|
out += esc(src.slice(last, m.index));
|
|
var t = m[0];
|
|
if (m[1]) out += '<span class="tc">' + esc(t) + "</span>";
|
|
else if (m[2] || m[3]) out += '<span class="ts">' + esc(t) + "</span>";
|
|
else if (kw.test(t)) out += '<span class="tk">' + t + "</span>";
|
|
else if (src.charAt(m.index + t.length) === "(") out += '<span class="tf">' + t + "</span>";
|
|
else out += esc(t);
|
|
last = m.index + t.length;
|
|
}
|
|
return out + esc(src.slice(last));
|
|
}
|
|
|
|
function paint(el) {
|
|
el.innerHTML = highlight(el.textContent, el.getAttribute("data-lang"));
|
|
}
|
|
|
|
var blocks = document.querySelectorAll(".side code[data-lang]");
|
|
for (var i = 0; i < blocks.length; i++) paint(blocks[i]);
|
|
|
|
/* ── contents: sidebar (desktop) + chips (mobile) ─────────── */
|
|
var groups = [].slice.call(document.querySelectorAll(".group"));
|
|
var entries = [].slice.call(document.querySelectorAll(".entry"));
|
|
var sideNav = document.getElementById("sideNav");
|
|
var contents = document.getElementById("contents");
|
|
var navLinks = {};
|
|
|
|
groups.forEach(function (group) {
|
|
var name = group.getAttribute("data-group");
|
|
var groupEntries = [].slice.call(group.querySelectorAll(".entry"));
|
|
|
|
var h = document.createElement("h2");
|
|
h.textContent = name;
|
|
sideNav.appendChild(h);
|
|
|
|
var ch = document.createElement("h2");
|
|
ch.textContent = name;
|
|
contents.appendChild(ch);
|
|
var ul = document.createElement("ul");
|
|
ul.className = "chips";
|
|
contents.appendChild(ul);
|
|
|
|
groupEntries.forEach(function (entry) {
|
|
var title = entry.querySelector("h3").textContent;
|
|
entry.dataset.search = (title + " " + entry.textContent).toLowerCase();
|
|
|
|
var a = document.createElement("a");
|
|
a.href = "#" + entry.id;
|
|
a.textContent = title.toLowerCase();
|
|
sideNav.appendChild(a);
|
|
navLinks[entry.id] = a;
|
|
|
|
var li = document.createElement("li");
|
|
var ca = document.createElement("a");
|
|
ca.href = "#" + entry.id;
|
|
ca.textContent = title.toLowerCase();
|
|
li.appendChild(ca);
|
|
ul.appendChild(li);
|
|
});
|
|
});
|
|
|
|
function counts() {
|
|
groups.forEach(function (group) {
|
|
var visible = group.querySelectorAll(".entry:not([hidden])").length;
|
|
var n = group.querySelector(".group-head .n");
|
|
if (n) n.textContent = visible + (visible === 1 ? " entry" : " entries");
|
|
});
|
|
}
|
|
|
|
/* ── filter ───────────────────────────────────────────────── */
|
|
var input = document.getElementById("filter");
|
|
var count = document.getElementById("count");
|
|
var empty = document.getElementById("empty");
|
|
var term = document.getElementById("term");
|
|
|
|
function apply() {
|
|
var q = input.value.trim().toLowerCase();
|
|
var shown = 0;
|
|
entries.forEach(function (entry) {
|
|
var hit = !q || entry.dataset.search.indexOf(q) !== -1;
|
|
entry.hidden = !hit;
|
|
if (navLinks[entry.id]) navLinks[entry.id].hidden = !hit;
|
|
if (hit) shown++;
|
|
});
|
|
groups.forEach(function (group) {
|
|
group.hidden = q.length > 0 &&
|
|
group.querySelectorAll(".entry:not([hidden])").length === 0;
|
|
});
|
|
empty.classList.toggle("on", shown === 0);
|
|
term.textContent = '"' + input.value.trim() + '"';
|
|
count.textContent = q ? shown + " of " + entries.length : entries.length + " constructs";
|
|
counts();
|
|
}
|
|
|
|
input.addEventListener("input", apply);
|
|
apply();
|
|
|
|
document.addEventListener("keydown", function (e) {
|
|
if (e.key === "/" && document.activeElement !== input) {
|
|
e.preventDefault();
|
|
input.focus();
|
|
} else if (e.key === "Escape" && document.activeElement === input) {
|
|
input.value = "";
|
|
apply();
|
|
input.blur();
|
|
}
|
|
});
|
|
|
|
/* ── scroll spy for the sidebar ───────────────────────────── */
|
|
if ("IntersectionObserver" in window) {
|
|
var current = null;
|
|
var io = new IntersectionObserver(function (obs) {
|
|
obs.forEach(function (o) {
|
|
if (o.isIntersecting) {
|
|
if (current) current.classList.remove("on");
|
|
current = navLinks[o.target.id];
|
|
if (current) current.classList.add("on");
|
|
}
|
|
});
|
|
}, { rootMargin: "-20% 0px -70% 0px" });
|
|
entries.forEach(function (e) { io.observe(e); });
|
|
}
|
|
|
|
/* ── hero: one pair at a time ─────────────────────────────── */
|
|
var pairs = [
|
|
['const total = 1;', 'total := 1'],
|
|
['xs.push(4);', 'xs = append(xs, 4)'],
|
|
['try { risky() }\ncatch (err) { handle(err) }', 'v, err := risky()\nif err != nil { handle(err) }'],
|
|
['await Promise.all([p1(), p2()]);', 'wg.Wait()'],
|
|
['class Dog extends Animal {}', 'type Dog struct{ Animal }'],
|
|
['user?.address?.city', 'if u != nil && u.Address != nil {}'],
|
|
['function* nums() { yield 1; }', 'for n := range nums { }'],
|
|
['app.get("/users/:id", handler)', 'mux.HandleFunc("GET /users/{id}", h)'],
|
|
['xs.map(x => x * 2)', 'for _, x := range xs { ... }']
|
|
];
|
|
|
|
var jsCell = document.getElementById("demoJs");
|
|
var goCell = document.getElementById("demoGo");
|
|
var at = 0;
|
|
|
|
function show(n) {
|
|
jsCell.innerHTML = highlight(pairs[n][0], "js");
|
|
goCell.innerHTML = highlight(pairs[n][1], "go");
|
|
}
|
|
show(0);
|
|
|
|
var still = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
if (!still.matches) {
|
|
setInterval(function () {
|
|
jsCell.classList.add("out");
|
|
goCell.classList.add("out");
|
|
setTimeout(function () {
|
|
at = (at + 1) % pairs.length;
|
|
show(at);
|
|
jsCell.classList.remove("out");
|
|
goCell.classList.remove("out");
|
|
}, 460);
|
|
}, 3800);
|
|
}
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|