DEV Community

Cover image for Rust Decorators for Go Developers
Andrei Merlescu
Andrei Merlescu

Posted on

Rust Decorators for Go Developers

In Go, most of these concerns are handled by conventions, comments, runtime reflection, and external tools. In Rust, they are expressed as attributes that the compiler reads, verifies, and enforces before your program ever runs.

If you have spent years writing Go, you already think in terms of goroutines, interfaces, and the implicit contract that the runtime handles the hard parts for you. Rust asks you to make all of those implicit contracts explicit — and the mechanism it uses to do that is attributes. Where Go relies on naming conventions, comments, build tags, struct tag strings, and external linters to express intent, Rust has a formal attribute syntax that the compiler reads at compile time, verifies, and enforces before a single line of your code runs. This document walks through thirteen categories of Rust attributes side by side with their closest Go analogs — not to argue that one language is better than the other, but to give you a map from the Go mental model you already own into the Rust one you are building. You will see that many things Go does silently behind its runtime — scheduling threads, copying memory, skipping serialized fields, enforcing interface contracts — Rust requires you to express openly through attributes, and in exchange the compiler guarantees correctness that Go leaves to discipline, convention, and luck. By the end you will understand not just what each attribute does, but why it exists and what problem it is solving that Go solves a different way.

#[derive(...)] — Automatic Trait Implementation

The Concept

#[derive(...)] tells the Rust compiler to automatically generate implementations of standard traits for your type. A trait in Rust is roughly what an interface is in Go, except the compiler can write the implementation for you when the logic is mechanical. Without #[derive(...)], you would have to manually implement Debug, Clone, PartialEq, and others for every struct and enum you write. The derive macro inspects your type at compile time and generates the boilerplate so you don’t have to.

The most commonly derived traits are: Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, and Serialize/Deserialize from serde.

Go

// Go has no equivalent syntax. The closest analog is implementing
// an interface by satisfying its method signatures manually.
// Go generates nothing for you — you write it all yourself.

type Person struct {
    Name string
    Age  int
}

// To print with structure, you implement Stringer manually:
func (p Person) String() string {
    return fmt.Sprintf("Person{Name: %s, Age: %d}", p.Name, p.Age)
}

// To copy, Go structs are value types — assignment copies automatically.
// To compare, == works on structs if all fields are comparable.
// None of this is declared — it's implicit or manual.
Enter fullscreen mode Exit fullscreen mode

Rust

// Rust generates all of this for you via #[derive(...)].
// Each trait listed is fully implemented by the compiler.

#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
struct Person {
    name: String,
    age: u32,
}

fn main() {
    let p1 = Person {
        name: "Andrei".to_string(),
        age: 40,
    };

    // Debug — enabled by #[derive(Debug)]
    // {:?} is the debug format, {:#?} is pretty-printed
    println!("{:?}", p1);
    println!("{:#?}", p1);

    // Clone — enabled by #[derive(Clone)]
    // .clone() performs a deep copy
    // Without this derive, .clone() does not compile
    let p2 = p1.clone();

    // PartialEq — enabled by #[derive(PartialEq)]
    // Without this, == does not compile on your struct
    assert_eq!(p1, p2);

    // Default — enabled by #[derive(Default)]
    // Returns a zero-value equivalent, like Go's zero value
    // String defaults to "", u32 defaults to 0
    let empty: Person = Person::default();
    println!("{:?}", empty);
}

// Copy vs Clone — critical distinction:
// Copy means the type can be duplicated by simply copying bits.
// Clone means the type knows how to duplicate itself, possibly with allocation.
// String cannot be Copy because it owns heap memory.
// u32, bool, f64 can be Copy because they are fixed-size stack values.

#[derive(Debug, Clone, Copy, PartialEq)]
struct Point {
    x: f64,
    y: f64,
}
// Point is Copy, so assignment moves a copy, not ownership.
// You can use p after assigning it to another variable.
Enter fullscreen mode Exit fullscreen mode

Key Concepts: If any field in your struct does not implement a trait you are trying to derive, the derive will fail to compile. For example, you cannot derive Copy on a struct that contains a String because String is not Copy. You cannot derive Eq without also deriving PartialEq. The order inside #[derive(...)] does not matter. You can stack multiple derives on the same line or split them across multiple #[derive(...)] attributes — both are valid. Deriving Default requires every field’s type to also implement Default.


#[arg(...)] — Clap CLI Argument Declaration

The Concept

#[arg(...)] is not a standard library attribute — it comes from the clap crate and decorates fields inside a #[derive(Parser)] struct. It describes how a struct field maps to a command-line argument: its flag name, short form, default value, environment variable fallback, help text, validation rules, and behavior. Clap reads these attributes at compile time and generates a full CLI parser from them.

Go

// Go's closest analog is the flag package, but it is entirely manual.
// There is no struct-level declaration — you register each flag imperatively.
// There is no compile-time validation — wrong types fail at runtime.

import "flag"

var find = flag.String("find", "", "substring to search for")
var cores = flag.Int("cores", 0, "number of cores to use")
var insensitive = flag.Bool("insensitive", true, "case insensitive search")

func main() {
    flag.Parse()
    // Values are now available via *find, *cores, *insensitive
    // No env var fallback built in
    // No conflict detection
    // No required field enforcement
}
Enter fullscreen mode Exit fullscreen mode

Rust

use clap::Parser;

#[derive(Parser, Debug)]
#[command(name = "find-xrp-addr", about = "XRP vanity address finder")]
struct Cli {

    // long:    creates --find flag
    // env:     falls back to $FIND if --find not passed
    // Type Option<String> means not required — None if absent
    #[arg(long, env = "FIND")]
    find: Option<String>,

    // long = "1p": overrides the field name for the CLI flag
    // action = SetTrue: presence of --1p sets this to true
    //                   without SetTrue, bool requires --1p=true explicitly
    // env: falls back to $USE_ONEPASSWORD
    #[arg(long = "1p", env = "USE_ONEPASSWORD", action = clap::ArgAction::SetTrue)]
    use_op: bool,

    // default_value_t: typed default, not a string — avoids parse step
    // 0 is sentinel meaning "use all available cores"
    #[arg(long, default_value_t = 0, env = "CORES")]
    cores: usize,

    // value_name: controls the placeholder in --help output
    // shows as: --begins <PREFIX>
    #[arg(long, value_name = "PREFIX", env = "BEGINS")]
    begins: Option<String>,

    // conflicts_with: errors if both --begins and --ends are
    // combined with --find in an unsupported way
    // requires: enforces another argument must also be present
    #[arg(long, env = "ENDS")]
    ends: Option<String>,

    // short and long together: accepts both -v and --vault
    #[arg(short, long, env = "VAULT")]
    vault: Option<String>,
}

fn main() {
    let cli = Cli::parse();
    println!("{:?}", cli);
}
Enter fullscreen mode Exit fullscreen mode

Key Concepts: default_value takes a &str and parses it, so a wrong string panics at runtime. default_value_t takes a typed expression and fails at compile time if the type is wrong — always prefer default_value_t. action = clap::ArgAction::SetTrue is required on bool fields if you want flag presence alone to mean true. Without it, the user must write --flag=true explicitly. Option<String> means absent is None — do not use required = true on an Option unless you explicitly want to override that inference.

BONUS: I wrote rusty-figtree so that you can use my figtree pattern in Rust, _instead of using #[arg()].


#[cfg(...)] — Conditional Compilation

The Concept

#[cfg(...)] tells the compiler to include or exclude a block of code depending on compile-time conditions: the target operating system, architecture, feature flags, test mode, or custom cfg values. The excluded code is completely removed from the binary — it is never parsed beyond syntax checking. This is more powerful than a runtime flag and more integrated than a preprocessor macro.

Go

// Go's closest analog is build tags — a comment at the top of a file
// that tells the Go toolchain whether to include that file.
// Go operates at the file level; Rust operates at the item level.

//go:build linux
// +build linux  (old syntax, kept for compatibility)

package main

// This entire file is excluded on non-Linux platforms.
// You cannot conditionally compile a single function inside a file in Go
// without splitting it into separate files.

func platformSpecificThing() {
    fmt.Println("Linux only")
}
Enter fullscreen mode Exit fullscreen mode

Rust

// #[cfg(...)] operates at the item level — a single function,
// struct, impl block, or expression can be conditionally included.

// Target OS conditions
#[cfg(target_os = "linux")]
fn platform_message() {
    println!("Running on Linux");
}

#[cfg(target_os = "macos")]
fn platform_message() {
    println!("Running on macOS");
}

#[cfg(target_os = "windows")]
fn platform_message() {
    println!("Running on Windows");
}

// Target architecture
#[cfg(target_arch = "x86_64")]
fn arch_note() {
    println!("64-bit x86");
}

// Feature flags — enabled via Cargo.toml [features] or --features flag
// This function only exists if the crate was compiled with --features="json"
#[cfg(feature = "json")]
fn serialize_json() {
    // json-specific code here
}

// Test-only code — #[cfg(test)] blocks are excluded from release builds
// This is the standard pattern for unit tests in Rust
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        assert_eq!(2 + 2, 4);
    }
}

// cfg! macro — evaluates to a bool at compile time for use in expressions
fn main() {
    if cfg!(debug_assertions) {
        println!("Running in debug mode");
    }

    // Combining conditions
    // all() = AND,  any() = OR,  not() = NOT
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    println!("Linux on x86_64 only");

    #[cfg(any(target_os = "linux", target_os = "macos"))]
    println!("Linux or macOS");

    #[cfg(not(target_os = "windows"))]
    println!("Not Windows");
}
Enter fullscreen mode Exit fullscreen mode

Key Concepts: Code inside a #[cfg(...)] block that is excluded is still syntax-checked but not type-checked in older Rust versions. In modern Rust it is fully excluded. This means you can have two functions with the same name under different #[cfg(...)] guards — this is the standard cross-platform pattern. The cfg!() macro is for runtime branching that the compiler still optimizes away. #[cfg(test)] is the canonical way to keep test helpers and unit tests out of production binaries — equivalent to Go's _test.go file convention but scoped to individual items rather than files.


#[macro_export] and #[macro_use] — Macro Visibility

The Concept

#[macro_export] makes a macro defined with macro_rules! publicly available to users of your crate, placing it at the crate root regardless of where in the module tree you defined it. #[macro_use] was the old way to import all macros from an external crate before Rust 2018 edition introduced use imports for macros. In modern Rust, #[macro_use] is mostly legacy but still seen in older codebases.

Go

// Go has no macros and no equivalent concept.
// The closest analog is a package-level exported function —
// you make something public by capitalizing its name.

package mylib

// Exported — visible to importers
func MyHelper(s string) string {
    return strings.ToUpper(s)
}

// Unexported — private to this package
func myHelper(s string) string {
    return strings.ToLower(s)
}

// There is no way in Go to define something that generates code
// at compile time the way Rust macros do.
// go generate exists but it runs external tools, not inline code.
Enter fullscreen mode Exit fullscreen mode

Rust

// Defining a macro and exporting it from your crate

// WITHOUT #[macro_export]:
// This macro is only usable within this module and its children.
macro_rules! say_hello {
    () => {
        println!("Hello");
    };
}

// WITH #[macro_export]:
// This macro is placed at the crate root and is importable by users
// of this crate with: use mycrate::greet;
#[macro_export]
macro_rules! greet {
    ($name:expr) => {
        println!("Hello, {}!", $name);
    };
    // Multiple arms — like match, first match wins
    ($name:expr, $greeting:expr) => {
        println!("{}, {}!", $greeting, $name);
    };
}

// In your own crate you can call it directly:
fn main() {
    greet!("Andrei");
    greet!("Andrei", "Good morning");
}

// ---

// #[macro_use] — OLD STYLE, pre-Rust-2018
// Importing all macros from an external crate without naming them:

#[macro_use]
extern crate serde_derive;

// Modern equivalent — explicit and preferred:
use serde::{Serialize, Deserialize};

// ---

// #[macro_use] on a module — imports all macros defined in that
// module into the parent scope. Still occasionally useful internally.

#[macro_use]
mod my_macros;
// Now all macros from my_macros are available in this module
// without qualifying them as my_macros::some_macro!(...)
Enter fullscreen mode Exit fullscreen mode

Key Concepts: #[macro_export] always places the macro at the crate root, regardless of nesting depth. If your macro is inside src/utils/helpers.rs, importing it is still use mycrate::my_macro, not use mycrate::utils::helpers::my_macro. This surprises many developers. In Rust 2018 and later, prefer explicit use mycrate::my_macro over #[macro_use] extern crate mycrate. The #[macro_use] attribute on extern crate is considered legacy. Procedural macros (#[derive(...)], #[some_attr], and function-like my_macro!(...) implemented in proc-macro crates) are a separate, more powerful system from macro_rules! and do not use #[macro_export].


#[repr(...)] — Memory Layout Control

The Concept

#[repr(...)] controls how Rust lays out a struct or enum in memory. By default Rust makes no guarantees about field ordering or padding — it may reorder fields to optimize alignment. #[repr(C)] forces C-compatible layout, which is required for FFI. #[repr(u8)] and similar force an enum’s discriminant to a specific integer type. #[repr(transparent)] guarantees a single-field struct has the same layout as its inner type.

Go

// Go structs have a defined layout: fields appear in declaration order,
// with padding inserted by the compiler for alignment.
// Go has no attribute to change this — you control layout by field order.

// Inefficient — bool causes padding before int64
type Bad struct {
    A bool    // 1 byte + 7 bytes padding
    B int64   // 8 bytes
}

// Efficient — largest fields first
type Good struct {
    B int64   // 8 bytes
    A bool    // 1 byte + 7 bytes padding at end
}

// For FFI with C, Go uses cgo and the layout matches C struct layout
// for exported types — but this is implicit, not declared.
Enter fullscreen mode Exit fullscreen mode

Rust

// Default: Rust may reorder fields freely for optimal packing.
// No guarantees. Do not transmute or use in FFI without #[repr(C)].
struct DefaultLayout {
    a: u8,
    b: u64,
    c: u8,
}
// Rust may reorder this to: b(u64), a(u8), c(u8) + padding

// #[repr(C)]: fields in declaration order, C-compatible padding.
// Required for any struct passed across FFI boundaries.
#[repr(C)]
struct CCompatible {
    a: u8,      // 1 byte
    // 7 bytes padding
    b: u64,     // 8 bytes
    c: u8,      // 1 byte
    // 7 bytes padding
}

// #[repr(packed)]: removes ALL padding. Fields may be unaligned.
// Accessing unaligned fields is undefined behavior in C.
// In Rust, taking a reference to a packed field is an error.
#[repr(packed)]
struct Packed {
    a: u8,
    b: u64,  // unaligned — no padding before it
}

// #[repr(transparent)]: single-field struct has identical layout
// to its inner type. Used for newtype wrappers in FFI.
#[repr(transparent)]
struct Meters(f64);
// Meters and f64 are interchangeable at the ABI level.

// #[repr(u8)] on an enum: discriminant stored as u8
// Without this, Rust picks the discriminant size freely.
#[repr(u8)]
enum Direction {
    North = 0,
    South = 1,
    East  = 2,
    West  = 3,
}

// #[repr(C)] on an enum: C-compatible tagged union layout
#[repr(C)]
enum CEnum {
    A,
    B,
    C,
}
Enter fullscreen mode Exit fullscreen mode

Key Concepts: Never use #[repr(packed)] unless you are parsing binary wire formats and know exactly what you are doing — unaligned access is undefined behavior on many architectures including ARM. #[repr(transparent)] is the correct way to build newtype wrappers for FFI — it guarantees the wrapper is a zero-cost abstraction with no layout difference from the inner type. #[repr(C)] is mandatory for any struct you pass to a C function via FFI. Forgetting it means Rust may reorder your fields and your C code reads garbage.


#[non_exhaustive] — Future-Proof Enums and Structs

The Concept

#[non_exhaustive] marks an enum or struct as potentially having more variants or fields added in future versions of the crate. External crates that match on a #[non_exhaustive] enum are required by the compiler to include a wildcard _ arm. This prevents downstream breakage when you add a new variant.

Go

// Go has no equivalent. The closest analog is the convention of
// documenting that a type "may grow" and relying on developer discipline.

// In Go, switching on an iota enum with no default case compiles fine.
// Adding a new value silently breaks exhaustive switches — no warning.

type Direction int
const (
    North Direction = iota
    South
    East
    West
)

func describe(d Direction) string {
    switch d {
    case North:
        return "north"
    case South:
        return "south"
    // No East, West — compiles fine, silently returns ""
    // If you add Northeast tomorrow, nothing tells callers to update
    }
    return ""
}
Enter fullscreen mode Exit fullscreen mode

Rust

// Without #[non_exhaustive]:
// External code that matches this enum must cover all variants.
// Adding a new variant is a breaking change.
pub enum Direction {
    North,
    South,
    East,
    West,
}

// With #[non_exhaustive]:
// External code MUST include a wildcard arm.
// You can add variants in future releases without breaking callers.
#[non_exhaustive]
pub enum Status {
    Active,
    Inactive,
    Pending,
    // You may add more variants in future versions.
}

// External crate trying to match Status:
fn describe(s: Status) -> &'static str {
    match s {
        Status::Active   => "active",
        Status::Inactive => "inactive",
        Status::Pending  => "pending",
        // This wildcard arm is REQUIRED by the compiler
        // because Status is #[non_exhaustive]
        _ => "unknown",
    }
}

// #[non_exhaustive] on a struct:
// External code cannot construct it with struct literal syntax.
// They must use your provided constructor functions.
#[non_exhaustive]
pub struct Config {
    pub timeout: u64,
    pub retries: u32,
    // You can add fields without breaking external callers
}

// External code cannot do this if Config is #[non_exhaustive]:
// let c = Config { timeout: 30, retries: 3 }; // compile error externally
// They must use: Config::new(30, 3) or a builder pattern
Enter fullscreen mode Exit fullscreen mode

Key Concepts: #[non_exhaustive] only affects code outside the defining crate. Inside the crate that defines the type, exhaustive matching still works and struct literals are still valid. This is the correct attribute to use on any public enum in a library crate where you anticipate adding variants in future semver-compatible releases. Without it, adding a variant is a breaking change that requires a major version bump.


#[must_use] — Enforced Return Value Handling

The Concept

#[must_use] on a function or type tells the compiler to emit a warning if the return value is discarded. It is how Rust enforces that callers handle Result and other important values. Without this attribute, silently ignoring a Result compiles without warning — which is how silent error swallowing happens.

Go

// Go's closest analog is the convention of always checking errors.
// The compiler does NOT warn you if you ignore a return value.
// Ignoring errors in Go is legal and silent.

os.Remove("file.txt")          // legal — error silently ignored
val, _ := strconv.Atoi("123") // legal — error explicitly discarded

// Go relies entirely on developer discipline and linters like errcheck.
// There is no language-level enforcement.
Enter fullscreen mode Exit fullscreen mode

Rust

// #[must_use] on a type:
// Any function returning this type triggers a warning if result is dropped.
// This is already on Result and Option in the standard library.

#[must_use]
enum MyResult<T> {
    Ok(T),
    Err(String),
}

// #[must_use] on a function:
// Warning if the return value of THIS specific function is discarded.
#[must_use]
fn compute_checksum(data: &[u8]) -> u32 {
    data.iter().fold(0u32, |acc, &b| acc.wrapping_add(b as u32))
}

fn main() {
    // This triggers: warning: unused return value of `compute_checksum`
    compute_checksum(&[1, 2, 3]);

    // This is fine — value is used
    let checksum = compute_checksum(&[1, 2, 3]);
    println!("{}", checksum);

    // Explicitly discarding — suppresses the warning
    // Use this when you genuinely mean to ignore the value
    let _ = compute_checksum(&[1, 2, 3]);

    // Result is #[must_use] in stdlib — this warns:
    std::fs::remove_file("file.txt");

    // This is fine:
    let _ = std::fs::remove_file("file.txt"); // explicit discard
    std::fs::remove_file("file.txt").ok();    // .ok() converts to Option, discards
    std::fs::remove_file("file.txt").unwrap(); // panics on error
    std::fs::remove_file("file.txt").expect("failed to remove"); // panics with message
}

// #[must_use] with a message:
#[must_use = "this seed was generated at cost — handle the result"]
fn generate_seed() -> [u8; 32] {
    [0u8; 32] // placeholder
}
Enter fullscreen mode Exit fullscreen mode

Key Concepts: Result<T, E> and Option<T> are both #[must_use] in the standard library. This is why the compiler warns you when you call a function returning Result and do nothing with it. The idiomatic ways to explicitly discard are let _ = ... for a one-off, or .ok() on a Result to convert it to an Option and implicitly drop it. Using #[must_use] on your own types and functions is considered good library hygiene — it prevents callers from making silent mistakes.


#[inline] — Inlining Hints

The Concept

#[inline] suggests to the compiler that a function’s body should be copied into every call site rather than generating a function call instruction. This eliminates call overhead and enables further optimizations at the call site. #[inline(always)] is a strong directive. #[inline(never)] prevents inlining, useful for debugging or code size control.

Go

// Go's compiler inlines automatically and aggressively.
// You cannot annotate functions with inlining hints in Go.
// The closest you can do is use //go:noinline to prevent it.

//go:noinline
func doNotInline(x int) int {
    return x * 2
}

// Otherwise, Go decides entirely on its own.
// You can observe inlining decisions with: go build -gcflags="-m"
Enter fullscreen mode Exit fullscreen mode

Rust

// #[inline] — hint to the compiler, may or may not be honored
// Most useful for small functions in library crates where the
// compiler cannot see across crate boundaries without LTO.
#[inline]
fn add(a: u32, b: u32) -> u32 {
    a + b
}

// #[inline(always)] — strong directive, almost always honored
// Use for hot path functions where call overhead is measurable.
// Overuse bloats binary size — every call site gets a copy.
#[inline(always)]
fn fast_clamp(val: f64, min: f64, max: f64) -> f64 {
    if val < min { min } else if val > max { max } else { val }
}

// #[inline(never)] — prevent inlining
// Useful when profiling — inlined functions disappear in stack traces.
// Also useful when a function is large and called from many places.
#[inline(never)]
fn expensive_operation(data: &[u8]) -> u64 {
    data.iter().map(|&b| b as u64).sum()
}

fn main() {
    let result = add(1, 2);       // likely inlined — no call instruction
    let clamped = fast_clamp(1.5, 0.0, 1.0);
    let sum = expensive_operation(&[1, 2, 3]);
    println!("{} {} {}", result, clamped, sum);
}
Enter fullscreen mode Exit fullscreen mode

Key Concepts: #[inline] is most important for functions in library crates. Within a single binary, the compiler can inline freely using its own judgment. Across crate boundaries, the compiler cannot inline unless the function is marked #[inline] or link-time optimization (LTO) is enabled. For hot-path cryptographic or search code like your XRP seed generator, #[inline(always)] on your innermost loop helpers can produce measurable gains. Do not use it globally — measure first.


#[test] — Unit Test Declaration

The Concept

#[test] marks a function as a unit test. The function is only compiled when running cargo test and is excluded from release builds. Test functions take no arguments and return either () or Result<(), E>. Combined with #[cfg(test)] on a module, this is the complete built-in testing story in Rust.

Go

// Go tests live in files ending in _test.go.
// Test functions start with Test and take *testing.T.
// The file is automatically excluded from non-test builds.

// file: mypackage_test.go
package mypackage

import "testing"

func TestAdd(t *testing.T) {
    result := add(1, 2)
    if result != 3 {
        t.Errorf("expected 3, got %d", result)
    }
}

// Subtests:
func TestMath(t *testing.T) {
    t.Run("addition", func(t *testing.T) {
        // ...
    })
    t.Run("subtraction", func(t *testing.T) {
        // ...
    })
}
Enter fullscreen mode Exit fullscreen mode

Rust

// Production code lives here — in the same file as tests.
fn add(a: u32, b: u32) -> u32 {
    a + b
}

// #[cfg(test)] means this entire module is excluded from non-test builds.
// It can see private functions in the parent module — tests are siblings,
// not external callers. This is different from Go where _test.go files
// can be in the same package (white-box) or a separate _test package (black-box).
#[cfg(test)]
mod tests {
    use super::*; // bring parent module's items into scope

    // #[test] marks this as a test function
    // cargo test discovers and runs all #[test] functions
    #[test]
    fn test_add() {
        assert_eq!(add(1, 2), 3);
    }

    // Tests can return Result — if Err is returned, the test fails
    #[test]
    fn test_with_result() -> Result<(), String> {
        let val = add(1, 2);
        if val != 3 {
            return Err(format!("expected 3, got {}", val));
        }
        Ok(())
    }

    // #[should_panic] — test passes only if the code panics
    #[test]
    #[should_panic(expected = "division by zero")]
    fn test_panic() {
        let _ = 1 / 0;
    }

    // #[ignore] — skip this test unless explicitly run with --ignored
    #[test]
    #[ignore]
    fn slow_integration_test() {
        // run with: cargo test -- --ignored
    }
}
Enter fullscreen mode Exit fullscreen mode

Key Concepts: Rust tests live inside the same file as production code inside a #[cfg(test)] module. This means they can access private functions directly — there is no need for a separate test package like Go's package foo_test. For integration tests that test your crate as an external user would, place them in a tests/ directory at the crate root — those files are automatically treated as separate crates with no access to private internals, equivalent to Go's package foo_test pattern.


#[bench] — Benchmark Declaration

The Concept

#[bench] marks a function as a benchmark, run with cargo bench. Benchmark functions receive a &mut Bencher and call b.iter(|| ...) with the code to measure. This is currently only available on nightly Rust in the standard library. Stable Rust benchmarking uses the criterion crate, which is the community standard.

Go

// Go benchmarks live in _test.go files alongside unit tests.
// Benchmark functions start with Benchmark and take *testing.B.

func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {
        add(1, 2)
    }
}
// Run with: go test -bench=. -benchmem
Enter fullscreen mode Exit fullscreen mode

Rust

// Nightly only — built-in bench:
#![feature(test)]
extern crate test;

#[cfg(test)]
mod benches {
    use super::*;
    use test::Bencher;

    // #[bench] marks this as a benchmark function
    // b.iter() runs the closure repeatedly and measures throughput
    #[bench]
    fn bench_add(b: &mut test::Bencher) {
        b.iter(|| {
            // test::black_box prevents the compiler from optimizing
            // the computation away entirely — critical for benchmarks
            test::black_box(add(1, 2))
        });
    }
}

// ---

// Stable Rust — criterion crate (preferred in production):
// Cargo.toml:
// [dev-dependencies]
// criterion = "0.5"
//
// [[bench]]
// name = "my_benchmark"
// harness = false

// benches/my_benchmark.rs:
use criterion::{black_box, criterion_group, criterion_main, Criterion};

fn bench_add(c: &mut Criterion) {
    c.bench_function("add 1+2", |b| {
        b.iter(|| add(black_box(1), black_box(2)))
    });
}

criterion_group!(benches, bench_add);
criterion_main!(benches);
Enter fullscreen mode Exit fullscreen mode

Key Concepts: black_box() is not optional. Without it, the compiler proves the result is unused and eliminates the entire computation — your benchmark measures zero work and reports impossibly fast numbers. This is equivalent to Go's _ = result pattern in benchmarks. Criterion on stable Rust is superior to the built-in nightly bencher: it uses statistical analysis, detects outliers, and produces HTML reports. For your seed generation benchmarks, criterion would give you confidence intervals across runs rather than a single number.


#[allow(...)], #[warn(...)], #[deny(...)], #[forbid(...)] — Lint Control

The Concept

These attributes control which compiler warnings and lints are active. #[allow(...)] suppresses a warning. #[warn(...)] enables a warning that may be off by default. #[deny(...)] turns a warning into a compile error. #[forbid(...)] turns a warning into a compile error and prevents any child scope from allowing it. They can be applied to a single item, a module, or the entire crate with #![...] (inner attribute syntax).

Go

// Go has no lint attributes — warnings are controlled externally
// via tools like go vet, staticcheck, or golangci-lint configuration.
// There is no in-code syntax to suppress a specific warning.
// The closest is a comment convention recognized by specific linters:

//nolint:errcheck
os.Remove("file.txt") // golangci-lint specific — not standard Go
Enter fullscreen mode Exit fullscreen mode

Rust

// Suppress a warning on a single item
#[allow(dead_code)]
fn unused_function() {
    println!("I exist but am never called");
}

// Suppress unused variable warning on a specific variable
fn main() {
    #[allow(unused_variables)]
    let x = 42;
    // x is never used — normally a warning, suppressed here
}

// Turn a warning into a compile error for a function
#[deny(unused_must_use)]
fn strict_function() {
    // Any ignored Result inside here is a compile error, not a warning
}

// Inner attribute syntax (#![...]) applies to the whole crate or module
// Place at the top of main.rs or lib.rs for crate-wide effect:

// Allow dead code across the whole crate (useful during development)
#![allow(dead_code)]

// Deny all warnings crate-wide — common in CI pipelines
#![deny(warnings)]

// Common lints worth knowing:
//
// dead_code           — function or field defined but never used
// unused_variables    — variable bound but never read
// unused_imports      — use statement that imports nothing used
// unused_must_use     — Result/must_use value silently dropped
// non_snake_case      — function or variable not in snake_case
// non_camel_case_types — type not in CamelCase
// clippy::all         — enables all clippy lints (requires clippy)
// clippy::pedantic    — stricter clippy lints

// #[forbid(...)] — cannot be overridden by any child #[allow(...)]
#[forbid(unsafe_code)]
mod safe_module {
    // No unsafe block is permitted anywhere in this module, period.
    // A child #[allow(unsafe_code)] would be a compile error.
}
Enter fullscreen mode Exit fullscreen mode

Key Concepts: #![deny(warnings)] at the crate root is common in CI but can make your build brittle when upgrading Rust versions — new lints become errors. A more robust pattern is #![deny(clippy::all)] combined with specific #[allow(...)] at the sites where you genuinely mean it. #[allow(dead_code)] is commonly needed on structs that are serialized/deserialized by serde — the fields look unused to the compiler because serde accesses them through reflection-like macros. #[forbid(unsafe_code)] is the correct way to document and enforce that a crate or module contains no unsafe Rust — stronger than a comment, enforced by the compiler.


#[deprecated] — Deprecation Notices

The Concept

#[deprecated] marks an item as deprecated. The compiler emits a warning at every call site that uses the deprecated item. You can include a message and a since version. It applies to functions, methods, structs, enums, traits, and type aliases.

Go

// Go has no deprecated attribute.
// The convention is a comment beginning with "Deprecated:"
// which godoc renders specially, and some linters detect.

// Deprecated: Use NewParser instead.
func Parse(s string) (*Result, error) {
    return NewParser().Parse(s)
}

// No compiler warning is emitted at call sites.
// Enforcement is entirely by convention and tooling.
Enter fullscreen mode Exit fullscreen mode

Rust

// Basic deprecation — warning emitted at every call site
#[deprecated]
fn old_function() {
    println!("I am old");
}

// With a message explaining the replacement
#[deprecated(note = "use new_function() instead")]
fn legacy_function() {
    new_function();
}

// With version information
#[deprecated(since = "2.0.0", note = "use new_function() instead")]
fn very_old_function() {}

fn new_function() {
    println!("I am new");
}

fn main() {
    // This compiles but emits: warning: use of deprecated function
    old_function();

    // To suppress the warning when you intentionally call deprecated code:
    #[allow(deprecated)]
    old_function();
}

// Deprecating a struct field:
struct Config {
    pub timeout: u64,
    #[deprecated(since = "1.5.0", note = "use timeout_ms instead")]
    pub timeout_secs: u64,
    pub timeout_ms: u64,
}
Enter fullscreen mode Exit fullscreen mode

Key Concepts: #[deprecated] on a trait method deprecates that method across all implementations. Call sites using the method get the warning regardless of which concrete type they hold. This is more powerful than Go's comment convention — the compiler actively notifies every downstream user. The since field is purely informational for documentation — the compiler does not enforce version checks.


#[serde(...)] — Serialization and Deserialization Attributes

The Concept

serde is Rust’s standard serialization framework. #[derive(Serialize, Deserialize)] on a struct generates serialization and deserialization code for any serde-compatible format (JSON, TOML, YAML, MessagePack, bincode, and many more). The #[serde(...)] attribute on fields and types customizes that generated code: renaming fields, skipping fields, providing defaults, and handling missing or null values.

Go

// Go uses struct tags — string literals on fields parsed at runtime
// via reflection. No compile-time checking of tag correctness.

type Person struct {
    Name      string `json:"name"`
    Age       int    `json:"age,omitempty"`
    Password  string `json:"-"`           // skip this field
    CreatedAt string `json:"created_at"`
}

// json.Marshal and json.Unmarshal read these tags at runtime.
// A typo in a tag compiles fine and silently does the wrong thing.
Enter fullscreen mode Exit fullscreen mode

Rust

use serde::{Serialize, Deserialize};

// Basic derive — generates Serialize and Deserialize implementations
// Works with any serde-compatible format
#[derive(Debug, Serialize, Deserialize)]
struct Person {

    // #[serde(rename = "...")] — different name in serialized form
    // Equivalent to Go's json:"name" tag
    #[serde(rename = "full_name")]
    name: String,

    // #[serde(skip_serializing_if = "...")] — conditional skip
    // skip_serializing_if takes a function path returning bool
    // Equivalent to Go's json:"age,omitempty"
    #[serde(skip_serializing_if = "Option::is_none")]
    age: Option<u32>,

    // #[serde(skip)] — never serialize or deserialize this field
    // Equivalent to Go's json:"-"
    // The field must implement Default for deserialization to work
    #[serde(skip)]
    internal_state: u64,

    // #[serde(default)] — use Default::default() if field is missing
    // during deserialization. Equivalent to Go's omitempty in reverse.
    #[serde(default)]
    verified: bool,

    // #[serde(default = "path::to::function")] — custom default
    #[serde(default = "default_role")]
    role: String,

    // #[serde(alias = "...")] — accept alternative names during deserialization
    #[serde(alias = "created_at", alias = "creation_date")]
    timestamp: String,

    // #[serde(flatten)] — inline the fields of a nested struct
    // The nested struct's fields appear at the same level in JSON
    #[serde(flatten)]
    metadata: Metadata,
}

fn default_role() -> String {
    "user".to_string()
}

#[derive(Debug, Serialize, Deserialize)]
struct Metadata {
    created_by: String,
    version: u32,
}

// #[serde(rename_all = "...")] — applies a naming convention to all fields
// Options: "camelCase", "snake_case", "PascalCase", "SCREAMING_SNAKE_CASE"
// Saves writing #[serde(rename = "...")] on every single field
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ApiResponse {
    user_id: u64,       // serializes as "userId"
    full_name: String,  // serializes as "fullName"
    is_active: bool,    // serializes as "isActive"
}

// #[serde(deny_unknown_fields)] — error on any unrecognized field
// during deserialization. Go's json.Decoder.DisallowUnknownFields() equivalent.
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct StrictConfig {
    host: String,
    port: u16,
}

// #[serde(tag = "type")] — internally tagged enum
// Adds a "type" field to distinguish variants in JSON
#[derive(Serialize, Deserialize)]
#[serde(tag = "type")]
enum Event {
    Login { user_id: u64 },
    Logout { user_id: u64 },
    Purchase { item_id: u64, amount: f64 },
}
// Login serializes as: {"type": "Login", "user_id": 123}
Enter fullscreen mode Exit fullscreen mode

Key Concepts: Go struct tags are runtime strings — a typo silently does the wrong thing. Serde attributes are compile-time — a typo is a compile error. #[serde(rename_all = "camelCase")] at the struct level is almost always what you want when building a JSON API in Rust — it handles the Go/Rust naming convention mismatch automatically. #[serde(flatten)] is the Rust answer to Go’s embedding — it inlines a nested struct’s fields into the parent’s serialized form. #[serde(deny_unknown_fields)] is essential for configuration file parsing where you want to catch typos in config keys rather than silently ignoring them.

Top comments (0)