MoonBit for Go Programmers#

MoonBit is a modern programming language designed for cloud and edge computing. If you're coming from Go, you'll find some familiar concepts alongside powerful new features that make MoonBit a simple yet expressive and performant language.

Key Similarities#

Both Go and MoonBit are:

  • Statically typed with type inference

  • Compiled languages with fast compilation

  • Memory safe, though through slightly different mechanisms

  • Designed for modern computing with excellent tooling

Major Differences at a Glance#

Aspect

Go

MoonBit

Paradigm

Imperative with certain functional features

Both functional and imperative

Memory Management

Garbage collected

Reference counting/GC (backend dependent)

Error Handling

Multiple return values

Checked error-throwing functions

Generics

Interfaces and type parameters

Full generic system with traits (similar to Rust)

Pattern Matching

Limited (switch statements)

Comprehensive pattern matching

Target Platforms

Native binaries

WebAssembly, JavaScript, native binaries (via C or LLVM)

Identifiers and Naming Conventions#

In Go, identifiers are case-sensitive and must start with a Unicode letter or an underscore, followed by any number of Unicode letters, Unicode digits, or underscores.

Since the first letter of a Go identifier dictates its visibility (uppercase for public, lowercase for private), the convention is to use camelCase for private items, and PascalCase for public ones:

type privateType int
var PublicVariable PublicType = PublicFunction()
privateVariable := privateFunction()

In MoonBit, identifiers are also case-sensitive and follow a very similar set of rules as Go, but the casing of the initial letter has no effect on visibility. Instead, lowercase initial letters should be used for variables and functions, while uppercase ones are reserved for types, traits, enumeration variants, etc.

As a result, the MoonBit convention is to use snake_case for the former category, and CamelCase for the latter:

Enumeration::Variant(random_variable).do_something()
impl[T : Trait] for Structure[T] with some_method(self, other) { ... }

Variable Bindings#

In Go, new bindings are created using var or :=. Type inference is activated by the := syntax or the omission of type annotation when using var. There is no way to mark a variable as immutable.

var name string = "MoonBit"
var count = 25 // Or `count := 25`
// There are no immutable variables in Go

In MoonBit, new bindings are created with the let keyword. They are immutable by default, and you can use let mut to create mutable ones. Types can be optionally specified with : after the variable name, and type inference is used in the absent case.

let mut name : String = "MoonBit"
let mut count = 25
let pi = 3.14159 // Omit `mut` to create an immutable binding

Note mut is only allowed in local bindings, and not allowed in global bindings.

Newtypes#

Newtypes are used to create type-safe wrappers around existing types, so that you can define domain-specific types with the same underlying representation as the original type, but with a different set of available operations.

In Go, newtypes can be created using the type keyword. A round trip from the underlying value to the newtype-wrapped one is possible via the T() conversion syntax:

type Age int

age := Age(25)
ageInt := int(age)

Newtypes are defined the same way in MoonBit, but getting the underlying value requires slightly different syntax:

type Age Int

let age = Age(25)
let age_int = age._

Type Aliases#

Type aliases can be created using the type ... = ... syntax in Go:

type Description = string

In MoonBit, the typealias keyword is used instead:

typealias String as Description

Structures#

In Go, named structures are newtypes of anonymous structures struct { ... }, hence the common type ... struct { ... } idiom:

type Person struct {
    Name string
    Age  int
}

The Person structure can be created using literals like so:

john := Person{
    Name: "John Doe",
    Age:  30,
}

// Field names can be omitted if the field order is respected:
alice := Person{"Alice Smith", 25}

In MoonBit, all structures must be named, and they are defined using the struct keyword:

struct Person {
  name : String
  age : Int
}

The Person structure can be created using literals like so:

let john = Person::{ name: "John Doe", age: 30 }

// Type name can be omitted if the type can be inferred:
let alice : Person = { name: "Alice Smith", age: 25 }

Enumerations#

Enumerations allow you to define a type with a fixed set of values.

In Go, enumeration is not a language feature, but rather an idiom of using iota to create a sequence of constants:

type Ordering int
const (
    Less Ordering = iota
    Equal
    Greater
)

On the other hand, MoonBit has a built-in enum keyword for defining enumerations:

enum Ordering {
  Less
  Equal
  Greater
}

In addition, MoonBit's enumerations can also have payloads:

enum IntList {
  /// `Nil` represents an empty list and has no payload.
  Nil
  /// `Cons` represents a non-empty list and has two payloads:
  /// 1. The first element of the list;
  /// 2. The remaining parts of the list.
  Cons(Int, IntList)
}

Control Flow#

One of MoonBit's key differences from Go is that lots of control structures have actual return values instead of simply being statements that execute code. This expression-centered approach allows for more concise and functional programming patterns compared to Go's statement-based control flow.

if Expressions#

In Go, if statements don't return values. As a result, you often need to write:

var result string
if condition {
    result = "true case"
} else {
    result = "false case"
}

In MoonBit, if is an expression that returns a value:

let result = if condition {
  "true case"
} else {
  "false case"
}

match Expressions#

In Go, switch statements can be used to match against values, but they don't return values directly:

var description string
switch err {
case nil:
    description = fmt.Sprintf("Success: %v", value)
default:
    description = fmt.Sprintf("Error: %s", err)
}

On the other hand, match expressions in MoonBit can actually return values:

let description = match status {
  Ok(value) => "Success: \{value}"
  Err(error) => "Error: \{error}"
}

For more details on match expressions, please refer to Pattern Matching.

loop Expressions#

MoonBit's loops can return values as well.

Functional loops using the loop keyword are particularly powerful. The loop body is similar to that of a match expression, where each arm tries to match the loop variables and act on them accordingly. You may use the continue keyword to start the next iteration of the loop with the given loop values, or use the break keyword to exit the loop with some given output value. At the trailing expression of each arm, the breaking is implicit and thus not required.

// Calculates the sum of all elements in an `xs : IntList`.
let sum = loop xs, 0 {
  Nil, acc => acc
  Cons(x, rest), acc => continue rest, x + acc
}

for and while Expressions#

MoonBit's for and while loops are also expressions that return values.

The for loop is similar to Go's for loop, with a variable initialization, condition, and update clause respectively:

// Iterates from 1 to 6, summing even numbers.
let sum = for i = 1, acc = 0; i <= 6; i = i + 1 {
  if i % 2 == 0 {
    continue i + 1, acc + i
  }
} else {
  acc
}

There are a few distinct features of the for loop in MoonBit, however:

  • The update clause is not in-place, but rather are used to assign new values to the loop variables.

  • continue can (optionally) be used to start the next iteration with new input values. In that case, the update clause is skipped.

  • The else clause is used to return the final value of the loop when it normally exits. If the loop is exited early with the break keyword, the value from the break clause is returned instead.

The while loop is equivalent to the for loop with a condition clause only, and it can also return a value:

let result = while condition {
  // loop body
  if should_break {
    break "early exit value"
  }
} else {
  "normal completion value"
}

Generic Types#

In Go, you can define a generic named structure using type parameters delimited by square brackets []:

type ListNode[T any] struct {
    val  T
    next *ListNode[T]
}

In MoonBit, you would define a generic structure very similarly:

struct ListNode[T] {
  val : T
  next : ListNode[T]?
}

In addition, you can also define a generic MoonBit enumeration. Below are two common generic enumerations available in MoonBit's standard library:

enum Option[T] {
  None
  Some(T)
}

enum Result[T, E] {
  Ok(T)
  Err(E)
}

Functions#

In Go, functions are defined using the func keyword followed by the function name, parameters, and return type.

func add(a int, b int) int {
    return a + b
}

In MoonBit, function definitions use the fn keyword and a slightly different syntax:

fn add(a : Int, b : Int) -> Int {
  a + b
}

Note the use of the : token to specify types, and the -> token to indicate the return type. Also, the function body is an expression that returns a value, so the return keyword is not required unless early exits are needed.

Value and Reference Semantics#

Understanding how data is passed and stored is crucial when moving between programming languages. Go and MoonBit, in particular, have different approaches to value and reference semantics.

In Go, the value semantics is the default. That is, values are copied when passed to functions or assigned to variables:

type Point struct {
    X int
    Y int
}

func modifyPointVal(p Point) {
    p.X = 100  // This modifies a copy, not the original
}

func main() {
    point := Point{X: 10, Y: 20}
    modifyPointVal(point)
    fmt.Println(point.X) // Still prints 10, not 100
}

To achieve reference semantics in Go, you often need to explicitly create and dereference pointers:

func modifyPointRef(p *Point) {
    p.X = 100  // This modifies the original through the pointer
}

func main() {
    point := Point{X: 10, Y: 20}
    modifyPointRef(&point)  // Create a pointer with the `&` operator
    fmt.Println(point.X)    // Now prints 100
}

Some other built-in types like slices and maps behave similarly to pointers in Go, but all these types are still technically passed by value (the value being a reference):

func incrementSlice(nums []int) {
    for i := range nums {
        nums[i]++  // Modifies original slice
    }
}

func modifyMap(m map[string]int) {
    m["key"] = 42  // Modifies original map
}

MoonBit is semantically always passed by reference. But for immutable types and primitive types, they may be passed by value since this is semantically the same, this is purely an optimization.

Notable primitive types in MoonBit include Unit , Boolean , integers (Int, Int64, UInt, etc.) , floating-point numbers (Double, Float, etc.) , String , Char , Byte.

Notable immutable collection types in MoonBit include tuples, immutable collections such as @immut/hashset.T[A], and custom types with no mut fields.

On the other hand, notable mutable collection types include mutable collections such as Array[T] , FixedArray[T] , and Map[K, V], as well as custom types with at least one mut field.

For example, we can rewrite some of the above Go examples in MoonBit:

struct Point {
  mut x : Int
  mut y : Int
}

fn modify_point_ref(p : Point) -> Unit {
  p.x = 100 // Modifies the original struct
}

fn main {
  let point = Point::{ x: 10, y: 20 }
  modify_point_ref(point) // Passes the original struct by reference
  println("\{point.x}")   // Prints 100
}

fn increment_array(nums : Array[Int]) -> Unit {
  for i = 0; i < nums.length(); i = i + 1 {
    nums[i] += 1 // Modifies the original array
  }
}

fn modify_map(m : Map[String, Int]) -> Unit {
  m["key"] = 42 // Modifies the original map
}

The Ref[T] Helper Type#

When you need explicit mutable references to value types, MoonBit provides the Ref[T] type which is roughly defined as follows:

struct Ref[T] {
  mut val : T
}

With the help of Ref[T], you can create mutable references to a value just like you would with pointers in Go:

fn increment_counter(counter : Ref[Int]) -> Unit {
  counter.val = counter.val + 1
}

fn main {
  let counter = Ref::new(0)
  increment_counter(counter)
  println(counter.val) // Prints 1
}

Generic Functions#

In Go, you can define a generic function using type parameters:

// `T` is a type parameter that must implement the `fmt.Stringer` interface.
func DoubleString[T fmt.Stringer](t T) string {
    s := t.String()
    return s + s
}

The same is true for MoonBit:

// `T` is a type parameter that must implement the `Show` trait.
fn[T : Show] double_string(t : T) -> String {
  let s = t.to_string()
  s + s
}

Named Parameters#

MoonBit functions also support named arguments with an optional default value using the label~ : Type syntax:

fn named_args(named~ : Int, optional~ : Int = 42) -> Int {
  named + optional
}

// This can be called like so:
named_args(named=10)               // optional defaults to 42
named_args(named=10, optional=20)  // optional is set to 20
named_args(optional=20, named=10)  // order doesn't matter
let named = 10
named_args(named~)                 // `label~` is a shorthand for `label=label`

Optional Return Values#

For functions that may or may not logically return a value of type T, Go encourages the use of multiple return values:

In particular, (res T, ok bool) is used to indicate an optional return value:

func maybeDivide(a int, b int) (quotient int, ok bool) {
    if b == 0 {
        return 0, false
    }
    return a / b, true
}

In MoonBit, to return an optional value, you will simply need to return T? (shorthand for Option[T]):

fn maybe_divide(a : Int, b : Int) -> Int? {
  if b == 0 {
    None
  } else {
    Some(a / b)
  }
}

Fallible Functions#

For functions that may return an error, Go uses (res T, err error) at the return position:

func divide(a int, b int) (quotient int, err error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

You can then use the above divide function with the common if err != nil idiom:

func useDivide() error {
    q, err := divide(10, 2)
    if err != nil {
        return err
    }
    fmt.Println(q) // Use the quotient
    return nil
}

In MoonBit, fallible functions are declared a bit differently. To indicate that the function might throw an error, write T!E at the return type position, where E is an error type declared with type!:

type! ValueError String

fn divide(a : Int, b : Int) -> Int!ValueError {
  if b == 0 {
    raise ValueError("division by zero")
  }
  a / b
}

There are different ways to handle the error when calling such a throwing function:

// Option 1: Propagate the error directly.
fn use_divide_propagate() -> Unit!ValueError {
  let q = divide(10, 2) // Rethrow the error if it occurs
  println(q) // Use the quotient
}

// Option 2: Use `try?` to convert the error to a `Result[T, E]` type.
fn use_divide_try() -> Unit!ValueError {
  let mq : Result[Int, ValueError] = // The type annotation is optional
    try? divide(10, 2)
  match mq { // Refer to the section on pattern matching for more details
    Err(e) => raise e
    Ok(q) => println(q) // Use the quotient
  }
}

// Option 3: Use the `try { .. } catch { .. }` syntax to handle the error.
fn use_divide_try_catch() -> Unit!ValueError {
  try {
    let q = divide(10, 2)
    println(q) // Use the quotient
  } catch {
    e => raise e
  }
}

Pattern Matching#

Go has no builtin support for pattern matching. In certain cases, you can use the switch statement to achieve similar functionality:

func fibonacci(n int) int {
    switch n {
    case 0:
        return 0
    case 1, 2:
        return 1
    default:
        return fibonacci(n-1) + fibonacci(n-2)
    }
}

MoonBit, on the other hand, offers comprehensive pattern matching with the match keyword.

You can match against literal values such as booleans, integers, strings, etc.:

fn fibonacci(n : Int) -> Int {
  match n {
    0 => 0
    // `|` combines multiple patterns
    1 | 2 => 1
    // `_` is a wildcard pattern that matches anything
    _ => fibonacci(n - 1) + fibonacci(n - 2)
  }
}

In addition, it is possible to perform destructuring of structures and tuples:

struct Point3D {
  x : Int
  y : Int
  z : Int
}

fn use_point3d(p : Point3D) -> Unit {
  match p {
    { x: 0, .. } => println("x == 0")
    // The `if` guard allows you to add additional conditions
    // for this arm to match.
    { y, z, .. } if y == z => println("x != 0, y == z")
    _ => println("uncategorized")
  }
}

Finally, array patterns allow you to easily destructure arrays, bytes, strings, and views:

fn categorize_array(array : Array[Int]) -> String {
  match array {
    [] => "empty"
    [x] => "only=\{x}"
    [first, .. middle, last] =>
      "first=\{first} and last=\{last} with middle=\{middle}"
  }
}

fn is_palindrome(s : @string.View) -> Bool {
  match s {
    [] | [_] => true
    // `a` and `b` capture the first and last characters of the view, and
    // `.. rest` captures the middle part of the view as a new view.
    [a, .. rest, b] if a == b => is_palindrome(rest)
    _ => false
  }
}

Methods and Traits#

Although both languages support methods and behavior sharing via traits/interfaces, MoonBit's approach to methods and traits/interfaces differs significantly from Go's.

As we will see below, MoonBit allows for more flexibility and expressiveness in terms of trait methods than go, since in MoonBit:

  • A trait must be explicitly implemented for each type;

  • A type's method is not necessarily object-safe (i.e. can be used in trait objects), in fact, they don't even need to have self as the first parameter at all.

Methods#

In Go, methods are defined on types using the receiver syntax:

type Rectangle struct {
    width, height float64
}

func (r *Rectangle) Area() float64 {
    return r.width * r.height
}

func (r *Rectangle) Scale(factor float64) {
    r.width *= factor
    r.height *= factor
}

Methods can be called using the .method() syntax:

rect := Rectangle{width: 10.0, height: 5.0}
areaValue := rect.Area()
rect.Scale(2.0) // NOTE: `.Scale()` modifies the rectangle in place.

In MoonBit, however, a type T's methods are simply functions defined with the T:: prefix.

This is how you would recreate the above Rectangle example in MoonBit:

struct Rectangle {
  // `mut` allows these fields to be modified in-place.
  mut width : Double
  mut height : Double
}

fn Rectangle::area(self : Rectangle) -> Double {
  self.width * self.height
}

fn Rectangle::scale(self : Rectangle, factor : Double) -> Unit {
  // NOTE: `self` have reference semantics here, since `Rectangle` is mutable.
  self.width *= factor
  self.height *= factor
}

... and you can call methods like this:

let rect = Rectangle::{ width: 10.0, height: 5.0 }
let area_value = rect.area()
rect.scale(2.0) // NOTE: `.scale()` modifies the rectangle in place.

Traits#

Go uses interfaces for polymorphism:

type Shape interface {
    Area() float64
    Perimeter() float64
}

// Implicitly implements the `Shape` interface for `Rectangle`
// `func (r *Rectangle) Area() float64` already exists
func (r *Rectangle) Perimeter() float64 {
    return 2 * (r.width + r.height)
}

// Static dispatch is possible using generic functions with type bounds
func PrintShapeInfo[T Shape](s T) {
    fmt.Printf("Area: %f, Perimeter: %f\n", s.Area(), s.Perimeter())
}

// Dynamic dispatch is possible using the interface type
func PrintShapeInfoDyn(s Shape) {
    PrintShapeInfo(s)
}

func TestRectangle(t *testing.T) {
    rect := &Rectangle{10, 5}
    PrintShapeInfo(rect)
    PrintShapeInfoDyn(rect)
}

MoonBit has traits, which are similar to Go interfaces:

trait Shape {
  area(Self) -> Double
  perimeter(Self) -> Double
}

// Explicitly implement the `Shape` trait for `Rectangle`
impl Shape for Rectangle with area(self) {
  // NOTE: This is a method call to the previously-defined `Rectangle::area()`,
  // thus no recursion is involved.
  self.area()
}

impl Shape for Rectangle with perimeter(self) {
  2.0 * (self.width + self.height)
}

// Static dispatch is possible using generic functions with type bounds
fn[T : Shape] print_shape_info(shape : T) -> Unit {
  println("Area: \{shape.area()}, Perimeter: \{shape.perimeter()}")
}

// Dynamic dispatch is possible using the `&Shape` trait object type
fn print_shape_info_dyn(shape : &Shape) -> Unit {
  print_shape_info(shape)
}

test {
  let rect = Rectangle::{ width: 10.0, height: 5.0 }
  print_shape_info(rect)
  print_shape_info_dyn(rect)
}

Object Safety#

MoonBit traits can also include certain kinds of methods not available in Go interfaces, such as the ones with no self parameter, as shown in the example below:

trait Name {
  name() -> String
}

impl Name for Rectangle with name() {
  "Rectangle"
}

// `T : Shape + Name` is a bound that requires the type `T` to
// implement both `Shape` and `Name`.
fn[T : Shape + Name] print_shape_name_and_info(shape : T) -> Unit {
  println(
    "\{T::name()}, Area: \{shape.area()}, Perimeter: \{shape.perimeter()}",
  )
}

test {
  print_shape_name_and_info(Rectangle::{ width: 10.0, height: 5.0 })
}

However, for a trait to be usable in a trait object, it must only contain object-safe methods.

There are a few requirements for a method of type T to be object-safe:

  • self : T should be the first parameter of the method;

  • Any other parameter of the method should not have the type T.

For example, in the above Name trait, the name() method is not object-safe because it does not have a self parameter, and thus Name cannot be used in the hypothetical &Name trait object.

Trait Extensions#

MoonBit traits can explicitly extend other traits, allowing you to build on existing functionality:

pub(open) trait Position {
  pos(Self) -> (Int, Int)
}

pub(open) trait Draw {
  draw(Self, Int, Int) -> Unit
}

pub(open) trait Object: Position + Draw {
  // You can add more required methods here...
}

Since the Object trait extends two traits Position and Draw, the latter two are called the former's supertraits.

Default Implementations#

Unlike Go interfaces, MoonBit trait functions can have default implementations:

trait Printable {
  print(Self) -> Unit
  // `= _` marks the method as having a default implementation
  print_twice(Self) -> Unit = _
}

// The default implementation of `print_twice()` is provided individually:
impl Printable with print_twice(self) {
  self.print()
  self.print()
}

Operator Overloading#

MoonBit supports operator overloading through built-in traits, which has no Go equivalent:

impl Add for Rectangle with op_add(self, other) {
  { width: self.width + other.width, height: self.height + other.height }
}

// Now you can use + with rectangles
let combined = rect1 + rect2

Imports and Package Management#

Package management and imports work differently between Go and MoonBit.

Creating a Project#

In Go, the first thing to do when creating a new project is running:

$ go mod init example.com/my-project

This will initialize a go.mod file that tracks your project's dependencies.

Then you can create a main.go file with the package main declaration and start writing your code:

package main

import "fmt"

func main() { fmt.Println("Hello, 世界") }

In MoonBit, creating a new project is much easier. Simply run the moon new command to set up your project:

$ moon new my-project

Project Structure#

The Go toolchain has few requirements for the project structure, apart from the go.mod file being in the root directory of the project. To scale up from a single main.go file to a larger project, you would typically add more files and directories, resulting in a flat or nested structure, depending on the style you choose.

While organizing the source files, the key point is that Go's tooling doesn't distinguish between source files under a common directory, so you can freely create multiple .go files in the same package directory, knowing that they will be treated as a whole by the toolchain. For definitions within source files of another directory, however, you would need to import them before they can be used.

In MoonBit, on the other hand, the default project structure provided by moon new is more organized, as shown below:

my-project
├── LICENSE
├── README.md
├── moon.mod.json
└── src
    ├── lib
    │   ├── hello.mbt
    │   ├── hello_test.mbt
    │   └── moon.pkg.json
    └── main
        ├── main.mbt
        └── moon.pkg.json

This demonstrates a typical "binary-and-library" project structure in MoonBit, located in the src directory. This is declared in moon.mod.json like so (with irrelevant parts omitted):

{
  "source": "src"
}

This is the module configuration file that also registers the project's basic information such as its name, version, and dependencies.

Each directory under the source directory (src in this example) is a package with its own moon.pkg.json file containing package-specific metadata, such as its imports, and whether it should be regarded as a main binary package. For example, src/lib/moon.pkg.json is minimally defined as follows:

{}

... and src/main/moon.pkg.json as follows:

{
  "is_main": true,
  "import": ["username/hello/lib"]
}

Similarly to Go, MoonBit treats all .mbt files under a same package directory as a whole. When creating a new directory for more source files, however, a corresponding moon.pkg.json file is required under that directory.

Running the Project#

To run the aforementioned Go project, you would typically use:

$ go run main.go

Running the previous MoonBit project with moon run is very similar:

$ moon run src/main/main.mbt

Adding Imports#

In Go, you can add imports with the import clause followed by their module path:

package main

import (
    "github.com/user/repo/sys"
)

MoonBit uses a different approach with moon.mod.json for module configuration and moon.pkg.json for package configuration.

First, declare dependencies in the "deps" section of your moon.mod.json. This is usually done with the moon add <package> command.

For example, to use moonbitlang/x, you would run:

$ moon add moonbitlang/x

... which would result in a moon.mod.json file like so (with irrelevant parts omitted):

{
  "deps": {
    "moonbitlang/x": "*"
  }
}

Then, in your package's moon.pkg.json, specify which packages to import in the "import" section:

{
  "import": ["moonbitlang/x/sys"]
}

Now you should be ready to use the sys package in this MoonBit package.

Using Imported Packages#

In Go, the above import allows you to access the sys package's APIs using the <package-name>. prefix (the actual API is hypothetical):

func main() {
    sys.SetEnvVar("MOONBIT", "Hello")
}

In MoonBit, you access imported APIs using the @<package-name>. prefix:

fn main {
  @sys.set_env_var("MOONBIT", "Hello")
}

Package Aliases#

In Go, you can create aliases for imported packages using the import statement:

import (
    system "github.com/user/repo/sys"
)

In MoonBit, you can create aliases for imported packages in moon.pkg.json using the alias field:

{
  "import": [
    {
      "path": "moonbitlang/x/sys"
      "alias": "system"
    }
  ]
}

Then you may use the alias like so:

fn main {
  @system.set_env_var("MOONBIT", "Hello")
}

Access Control#

In Go, visibility is determined by the case of the first letter of an identifier (uppercase for public, lowercase for private).

MoonBit has more granular access control than Go, providing the following visibility levels:

  • priv: Completely private (like Go's lowercase identifiers)

  • Default (abstract): Only the type name is visible, implementation is hidden

  • pub: Read-only access from outside the package

  • pub(all): Full public access (like Go's uppercase identifiers)

This gives you fine-grained control over what parts of your API are exposed and how they can be used.

Runtime Support#

The same MoonBit code can target multiple runtimes with different code generation backends, allowing you to choose the best fit for your particular application:

  • WebAssembly (for web and edge computing)

  • JavaScript (for Node.js integration)

  • C (for native performance)

  • LLVM (experimental)

Memory Management#

Go uses a garbage collector, while MoonBit uses different strategies depending on the code generation backend being used:

  • Wasm/C backends: Reference counting without cycle detection

  • Wasm GC/JavaScript backends: Leverages the runtime's garbage collector

Getting Started#

  1. Visit the online playground.

  2. Check out our installation guide.

  3. Create your first MoonBit project:

    $ moon new hello-world
    $ cd hello-world
    $ moon run
    

When to Choose MoonBit#

MoonBit offers a fresh take on systems programming with functional programming benefits and WebAssembly-first design. While different from Go's philosophy, it provides powerful tools for building efficient, safe, and maintainable applications. Thus, MoonBit will be an interesting option for your project if you embrace:

  • WebAssembly targets with minimal size and maximum performance

  • Functional programming features like pattern matching and algebraic data types

  • Mathematical/algorithmic code that benefits from immutability by default

  • Strong type safety with comprehensive error handling

Next Steps#