Migrating from Go to Gossamer¶
Gossamer keeps Go's practical concurrency model: goroutines,
channels, select, and defer are first-class. The surface syntax is
closer to Rust, so the largest migration cost is mechanical syntax plus
more explicit types and errors.
Quick Map¶
| Go | Gossamer | Notes |
|---|---|---|
func f(x int) int { return x + 1 } |
fn f(x: i64) -> i64 { x + 1 } |
return is optional for the final expression. |
x := 5 |
let x = 5 |
Use let mut when rebinding. |
type Point struct { X int; Y int } |
struct Point { x: i64, y: i64 } |
Named structs are constructed with braces. |
Point{X: 1, Y: 2} |
Point { x: 1, y: 2 } |
Named fields. |
Point{1, 2} |
Point { x: 1, y: 2 } |
Named structs require keyed fields. |
| tuple-like constructor | enum Msg { Data(String) } then Msg::Data("x") |
Parentheses are for enum variants and tuple structs, not named structs. |
func (p Point) Norm() int |
impl Point { fn norm(&self) -> i64 { ... } } |
Methods live in impl blocks. |
type Reader interface { Read([]byte) int } |
trait Reader { fn read(&self, buf: &mut [u8]) -> i64 } |
Traits are nominal. |
if err != nil { return err } |
let v = f()? |
? propagates Err. |
go work() |
go work() |
Same idea. |
defer cleanup() |
defer cleanup() |
Same idea. |
ch <- v |
tx.send(v) |
Channels use sender and receiver handles. |
v, ok := <-ch |
while let Some(v) = rx.recv() { ... } |
None means the channel is closed. |
make([]int, 0, 16) |
Vec::<i64>::with_capacity(16) |
Vec<T> owns growable storage; &[T] is a borrowed slice view. |
make(map[string]int) |
Map::<String, i64>::new() |
Import from std::collections. |
map[string]int{"k": 1} |
{"k": 1} |
Map literal. |
set via map[T]struct{} |
#{...} |
Set literal, or typed BTreeSet<T> for ordered sets. |
| FIFO queue slice | Queue::from([1, 2]) |
push appends, pop removes from the front. |
| stack slice | Stack::from([1, 2]) |
push appends, pop removes from the top. |
container/heap |
MinHeap::from([...]), or MaxHeap::from([...]) |
Heap operations are push, pop, and peek. |
container/list or ring-buffer deque |
Deque<i64> |
Use explicit front/back methods. |
Entry files may use top-level statements. Items are hoisted, and bare
statements become the body of an implicit fn main().
Gossamer 0.47 Syntax At A Glance¶
Go permits implicit statement termination but still uses commas in multiline
composite literals. Gossamer permits semicolons only between same-line
statements and uses a stricter layout rule:
commas separate items on one line, while newlines separate items in a
multiline delimited list. Legacy multiline commas parse, but gos fmt removes
them.
struct User {
name: String
active: bool
}
fn rename(
user: User
name: String
) -> User {
User {
name: name
active: user.active
}
}
enum Lookup {
Found {
index: i64
user: User
}
Missing(String)
}
let user = User { name: "Ada", active: true } // one line needs commas
Named structs require keyed braces. Parentheses construct tuple structs and tuple enum variants. Collection and field access is explicit:
let users = #[user, rename(user, "Grace")]
let first = users[0] // slice/Vec index; traps if out of bounds
let initial = first.name[0] // String index is a UTF-8 byte as i64
let pair = (first.name, first.active)
let enabled = pair.1
let mut by_name: Map<String, User> = Map::new()
by_name.insert(first.name, first)
let cached = by_name.get("Ada") // Map lookup returns Option<V>
let found = Lookup::Found {
index: 0
user: cached.unwrap()
}
Errors¶
Go usually returns (value, error). Gossamer uses Result<T, E>:
use std::{errors, fs}
fn run() -> Result<(), errors::Error> {
let data = fs::read("config.toml")?
println!("{}", data.len())
Ok(())
}
Use Option<T> for values that may be absent. There is no nil pointer.
Interfaces And Traits¶
Go interfaces are structural. A type satisfies an interface when it has the right methods. Gossamer traits are nominal, so the conformance is explicit:
trait Writer {
fn write(&mut self, data: &[u8]) -> Result<i64, errors::Error>
}
struct Buffer { data: [u8] }
impl Writer for Buffer {
fn write(&mut self, data: &[u8]) -> Result<i64, errors::Error> {
for b in data {
self.data.push(b)
}
Ok(data.len() as i64)
}
}
Generic bounds use T: Trait. Runtime trait objects are not the default
escape hatch; prefer generics or a closed enum plus match.
Concurrency¶
Channels are created with channel::<T>(). channel() and
channel(0) are unbuffered, channel(n) is bounded, and
channel::unbounded() is explicitly unbounded.
let (tx, rx) = channel::<i64>()
go fn() {
defer tx.close()
for n in 0..3 {
tx.send(n)
}
}()
while let Some(n) = rx.recv() {
println!("{n}")
}
select is Go-shaped:
select {
v = rx.recv() => println!("got {v}")
tx.send(42) => println!("sent")
default => println!("would block")
}
HTTP¶
Handlers implement http::Handler and return Result<http::Response,
http::Error> when they can fail:
use std::http
struct App { }
impl http::Handler for App {
fn serve(&self, r: http::Request) -> Result<http::Response, http::Error> {
if r.path() == "/bytes" {
return Ok(http::Response {
status: 200
body: [65, 0, 66]
content_type: "application/octet-stream"
})
}
Ok(http::Response::text(200, "hello\n"))
}
}
fn main() {
if let Err(e) = http::serve("127.0.0.1:8080", App { }) {
eprintln!("serve failed: {e}")
}
}
http::get(url, headers) returns Result<http::Response,
errors::Error>. Pass [] when there are no headers.
SQL¶
std::database::sql is a driver registry and wrapper surface. Drivers
register themselves at startup. User code normally opens a connection
through sql::open(driver, dsn) or a pool through sql::Pool::open.
use std::database::sql
fn count_users() -> Result<i64, sql::Error> {
let mut db = sql::open("sqlite", "file:app.db")?
let mut rows = db.query("select id from users", &[])?
defer rows.close()
let mut count = 0
while let Some(_row) = rows.next_row()? {
count += 1
}
Ok(count)
}
Do not construct driver wrapper structs directly. They are real named structs now, so when a fixture does need a literal, it uses braces.
Collections And Pipelines¶
Gossamer keeps ordinary loops for side effects and early returns.
Transformation pipelines use free functions in std::iter with the
data argument last:
use std::iter
let total = #[1, 2, 3, 4, 5]
|> iter::filter(|n: i64| n % 2 == 0)
|> iter::sum_by(|n: i64| n * n)
The same pipe-friendly shape exists for std::option and
std::result.
Visibility¶
Gossamer has three visibilities, and they are declared per item, per method, and per struct field.
| Annotation | Reachable from |
|---|---|
| none | the declaring module and its descendants |
pub(package) |
every module of the declaring package |
pub |
anything that depends on the package |
A package is the unit of distribution: one project.toml, one project id.
A module is a directory under src/. A module nested inside another is a
module descendant, and visibility flows inward only: a descendant reaches
its ancestors' private items, never the reverse.
// src/money/mod.gos
pub struct Amount {
pub currency: String,
cents: i64, // private representation
}
impl Amount {
pub fn new(currency: String, cents: i64) -> Amount {
Amount { currency: currency, cents: cents }
}
pub fn cents(&self) -> i64 { self.cents }
fn normalize(&self) -> i64 { self.cents } // private helper
}
pub(package) fn round_trip(a: &Amount) -> i64 { a.normalize() }
A pub type may keep private methods and private fields, so a struct with any
private field can only be built by the module that declares it. Importing does
not widen anything: a use is a spelling convenience, and visibility is
decided by where the name is used.
Coming from Go, the biggest change is that visibility is
declared, not inferred from capitalization. Amount and amount are equally
public or private depending on the pub you write. Go's package-level
visibility maps to pub(package); Go has no equivalent of pub, because a
Go package's exported surface is exported to everyone.
Common Ports¶
| Go | Gossamer |
|---|---|
os.ReadFile(path) |
fs::read(path) |
os.ReadFile as text |
fs::read_to_string(path) |
os.WriteFile(path, data, 0644) |
fs::write(path, data) |
os.Getenv("NAME") |
env::var("NAME") |
os.Args |
env::args() |
exec.Command(name, args...).Run() |
process::run(name, &args) |
strings.TrimSpace(s) |
strings::trim(&s) |
strconv.Atoi(s) |
strconv::parse_i64(&s) |
time.Sleep(d) |
time::sleep(ms) |
sync.WaitGroup |
sync::WaitGroup |
net/http server |
std::http |
| WebSocket handler | std::http::websocket |
| SSE handler | std::http::sse |