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 { 1, 2 } |
Positional struct literals also use braces. |
| 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) |
[T] is the slice/vector type spelling in Gossamer source. |
make(map[string]int) |
HashMap::<String, i64>::new() |
Import from std::collections. |
Entry files may use top-level statements. Items are hoisted, and bare
statements become the body of an implicit fn main().
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.
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 |