Skip to content

Latest commit

 

History

History
1340 lines (914 loc) · 27.6 KB

docs.md

File metadata and controls

1340 lines (914 loc) · 27.6 KB

Go+ Quick Start

Our vision is to enable everyone to create production-level applications.

Easy to learn

  • Simple and easy to understand
  • Smaller syntax set than Python in best practices

Ready for large projects

  • Derived from Go and easy to build large projects from its good engineering foundation

The Go+ programming language is designed for engineering, STEM education, and data science.

  • For engineering: working in the simplest language that can be mastered by children.
  • For STEM education: studying an engineering language that can be used for work in the future.
  • For data science: communicating with engineers in the same language.

How to install

on Windows

winget install goplus.gop

on Debian/Ubuntu

sudo bash -c ' echo "deb [trusted=yes] https://pkgs.goplus.org/apt/ /" > /etc/apt/sources.list.d/goplus.list'
sudo apt update
sudo apt install gop

on RedHat/CentOS/Fedora

sudo bash -c 'echo -e "[goplus]\nname=Go+ Repo\nbaseurl=https://pkgs.goplus.org/yum/\nenabled=1\ngpgcheck=0" > /etc/yum.repos.d/goplus.repo'
sudo yum install gop

on macOS/Linux (Homebrew)

Install via brew

$ brew install goplus

from source code

Note: Requires go1.18 or later

git clone https://github.com/goplus/gop.git
cd gop

# On mac/linux run:
./all.bash
# On Windows run:
all.bat

Actually, all.bash and all.bat will use go run cmd/make.go underneath.

Running in Go+ playground

If you don't want install Go+, you can write your Go+ programs in Go+ playground. This is the fastest way to experience Go+.

And you can share your Go+ code with your friends. Here is my Hello world program:

Table of Contents

Hello World

Different from the function call style of most languages, Go+ recommends command style code:

println "Hello world"

Save this snippet into a file named hello.gop. Now do: gop run hello.gop.

Congratulations - you just wrote and executed your first Go+ program!

You can compile a program without execution with gop build hello.gop. See gop help for all supported commands.

println is one of the few built-in functions. It prints the value passed to it to standard output.

To emphasize our preference for command style, we introduce echo as an alias for println:

echo "Hello world"

See https://tutorial.goplus.org/hello-world for more details.

Running a project folder with several files

Suppose you have a folder with several .gop files in it, and you want to compile them all into one program. Just do: gop run ..

Passing parameters also works, so you can do: gop run . --yourparams some_other_stuff.

Your program can then use the CLI parameters like this:

import "os"

println os.Args

Comments

# This is a single line comment.

// This is a single line comment.

/*
This is a multiline comment.
*/

Variables

name := "Bob"
age := 20
largeNumber := int128(1 << 65)
println name, age
println largeNumber

Variables are declared and initialized with :=.

The variable's type is inferred from the value on the right hand side. To choose a different type, use type conversion: the expression T(v) converts the value v to the type T.

Initialization vs. assignment

Note the (important) difference between := and =. := is used for declaring and initializing, = is used for assigning.

age = 21

This code will not compile, because the variable age is not declared. All variables need to be declared in Go+.

age := 21

The values of multiple variables can be changed in one line. In this way, their values can be swapped without an intermediary variable.

a, b := 0, 1
a, b = b, a
println a, b // 1, 0

Go+ Types

Primitive types

bool

int8    int16   int32   int    int64    int128
uint8   uint16  uint32  uint   uint64   uint128

uintptr // similar to C's size_t

byte // alias for uint8
rune // alias for int32, represents a Unicode code point

string

float32 float64

complex64 complex128

bigint bigrat

unsafe.Pointer // similar to C's void*

any // alias for Go's interface{}

Strings

name := "Bob"
println name.len  // 3
println name[0]   // 66
println name[1:3] // ob
println name[:2]  // Bo
println name[2:]  // b

// or using octal escape `\###` notation where `#` is an octal digit
println "\141a"   // aa

// Unicode can be specified directly as `\u####` where # is a hex digit
// and will be converted internally to its UTF-8 representation
println "\u2605"  // ★

String values are immutable. You cannot mutate elements:

s := "hello 🌎"
s[0] = `H` // not allowed

Note that indexing a string will produce a byte, not a rune nor another string.

Strings can be easily converted to integers:

s := "12"
a, err := s.int
b := s.int! // will panic if s isn't a valid integer

String operators

name := "Bob"
bobby := name + "by" // + is used to concatenate strings
println bobby // Bobby

s := "Hello "
s += "world"
println s // Hello world

Most Go+ operators must have values of the same type on both sides. You cannot concatenate an integer to a string:

age := 10
println "age = " + age // not allowed

We have to either convert age to a string:

age := 10
println "age = " + age.string

However, you can replace age.string to "${age}":

age := 10
println "age = ${age}"

Here is a more complex example of ${expr}:

host := "example.com"
page := 0
limit := 20
println "https://${host}/items?page=${page+1}&limit=${limit}" // https://example.com/items?page=1&limit=20
println "$$" // $

Runes

A rune represents a single Unicode character and is an alias for int32.

rocket := '🚀'
println rocket         // 128640
println string(rocket) // 🚀

Numbers

a := 123

This will assign the value of 123 to a. By default a will have the type int.

You can also use hexadecimal, binary or octal notation for integer literals:

a := 0x7B
b := 0b01111011
c := 0o173

All of these will be assigned the same value, 123. They will all have type int, no matter what notation you used.

Go+ also supports writing numbers with _ as separator:

num := 1_000_000 // same as 1000000

If you want a different type of integer, you can use casting:

a := int64(123)
b := uint8(12)
c := int128(12345)

Assigning floating point numbers works the same way:

f1 := 1.0
f2 := float32(3.14)

If you do not specify the type explicitly, by default float literals will have the type of float64.

Float literals can also be declared as a power of ten:

f0 := 42e1   // 420
f1 := 123e-2 // 1.23
f2 := 456e+2 // 45600

Go+ has built-in support for rational numbers:

a := 1r << 200  // suffix `r` means `rational`
b := bigint(1 << 200)

And you can cast bool to number types (this is NOT supported in Go):

println int(true)       // 1
println float64(true)   // 1
println complex64(true) // (1+0i)

Slices

A slice is a collection of data elements of the same type. A slice literal is a list of expressions surrounded by square brackets. An individual element can be accessed using an index expression. Indexes start from 0:

nums := [1, 2, 3]
println nums      // [1 2 3]
println nums.len  // 3
println nums[0]   // 1
println nums[1:3] // [2 3]
println nums[:2]  // [1 2]
println nums[2:]  // [3]

nums[1] = 5
println nums // [1 5 3]

Type of a slice literal is infered automatically.

a := [1, 2, 3]   // []int
b := [1, 2, 3.4] // []float64
c := ["Hi"]      // []string
d := ["Hi", 10]  // []any
d := []          // []any

And casting slice literals also works.

a := []float64([1, 2, 3]) // []float64

Maps

a := {"Hello": 1, "xsw": 3}     // map[string]int
b := {"Hello": 1, "xsw": 3.4}   // map[string]float64
c := {"Hello": 1, "xsw": "Go+"} // map[string]any
d := {}                         // map[string]any

If a key is not found, a zero value is returned by default:

a := {"Hello": 1, "xsw": 3}
c := {"Hello": 1, "xsw": "Go+"}
println a["bad_key"] // 0
println c["bad_key"] // <nil>

You can also check, if a key is present, and get its value.

a := {"Hello": 1, "xsw": 3}
if v, ok := a["xsw"]; ok {
    println "its value is", v
}

Module imports

For information about creating a module, see Modules.

Modules can be imported using the import keyword:

import "strings"

x := strings.NewReplacer("?", "!").Replace("Hello, world???")
println x // Hello, world!!!

Module import aliasing

Any imported module name can be aliased:

import strop "strings"

x := strop.NewReplacer("?", "!").Replace("Hello, world???")
println x // Hello, world!!!

Statements & expressions

If..else

In Go+, if statements are pretty straightforward and similar to most other languages. Unlike other C-like languages, there are no parentheses surrounding the condition and the braces are always required.

a := 10
b := 20
if a < b {
    println "a < b"
} else if a > b {
    println "a > b"
} else {
    println "a == b"
}

For loop

Go+ has only one looping keyword: for, with several forms.

for/<-

This is the most common form. You can use it with a slice, map, numeric range or custom iterators.

For information about creating a custom iterators, see Custom iterators.

Slice for

The for value <- arr form is used for going through elements of a slice.

numbers := [1, 3, 5, 7, 11, 13, 17]
sum := 0
for x <- numbers {
    sum += x
}
println sum // 57

If an index is required, an alternative form for index, value <- arr can be used.

names := ["Sam", "Peter"]
for i, name <- names {
    println i, name
    // 0 Sam
    // 1 Peter
}
Map for
m := {"one": 1, "two": 2}
for key, val <- m {
    println key, val
    // one 1
    // two 2
}
for key, _ <- m {
    println key
    // one
    // two
}
for val <- m {
    println val
    // 1
    // 2
}
Range for

You can use range expression (start:end:step) in for loop.

for i <- :5 {
    println i
    // 0
    // 1
    // 2
    // 3
    // 4
}
for i <- 1:5 {
    println i
    // 1
    // 2
    // 3
    // 4
}
for i <- 1:5:2 {
    println i
    // 1
    // 3
}
for/<-/if

All loops of for/<- form can have an optional if condition.

numbers := [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
for num <- numbers if num%3 == 0 {
    println num
    // 0
    // 3
    // 6
    // 9
}

for num <- :10 if num%3 == 0 {
    println num
    // 0
    // 3
    // 6
    // 9
}

Condition for

sum := 0
i := 1
for i <= 100 {
    sum += i
    i++
}
println sum // 5050

This form of the loop is similar to while loops in other languages. The loop will stop iterating once the boolean condition evaluates to false. Again, there are no parentheses surrounding the condition, and the braces are always required.

C for

for i := 0; i < 10; i += 2 {
    // Don't print 6
    if i == 6 {
        continue
    }
    println i
    // 0
    // 2
    // 4
    // 8
}

Finally, there's the traditional C style for loop. It's safer than the while form because with the latter it's easy to forget to update the counter and get stuck in an infinite loop.

Bare for

for {
    // ...
}

The condition can be omitted, resulting in an infinite loop. You can use break or return to end the loop.

Error handling

We reinvent the error handling specification in Go+. We call them ErrWrap expressions:

expr! // panic if err
expr? // return if err
expr?:defval // use defval if err

How to use them? Here is an example:

import (
    "strconv"
)

func add(x, y string) (int, error) {
    return strconv.Atoi(x)? + strconv.Atoi(y)?, nil
}

func addSafe(x, y string) int {
    return strconv.Atoi(x)?:0 + strconv.Atoi(y)?:0
}

println `add("100", "23"):`, add("100", "23")!

sum, err := add("10", "abc")
println `add("10", "abc"):`, sum, err

println `addSafe("10", "abc"):`, addSafe("10", "abc")

The output of this example is:

add("100", "23"): 123
add("10", "abc"): 0 strconv.Atoi: parsing "abc": invalid syntax

===> errors stack:
main.add("10", "abc")
    /Users/xsw/tutorial/15-ErrWrap/err_wrap.gop:6 strconv.Atoi(y)?

addSafe("10", "abc"): 10

Compared to corresponding Go code, It is clear and more readable.

And the most interesting thing is, the return error contains the full error stack. When we got an error, it is very easy to position what the root cause is.

How these ErrWrap expressions work? See Error Handling for more information.

Functions

func add(x int, y int) int {
    return x + y
}

println add(2, 3) // 5

Returning multiple values

func foo() (int, int) {
    return 2, 3
}

a, b := foo()
println a // 2
println b // 3
c, _ := foo() // ignore values using `_`

Variadic parameters

func sum(a ...int) int {
    total := 0
    for x <- a {
        total += x
    }
    return total
}

println sum(2, 3, 5) // 10

Output parameters can have names.

func sum(a ...int) (total int) {
    for x <- a {
        total += x
    }
    return // don't need return values if they are assigned
}

println sum(2, 3, 5) // 10

Higher order functions

Functions can also be parameters.

func square(x float64) float64 {
    return x*x
}

func abs(x float64) float64 {
    if x < 0 {
        return -x
    }
    return x
}

func transform(a []float64, f func(float64) float64) []float64 {
    return [f(x) for x <- a]
}

y := transform([1, 2, 3], square)
println y // [1 4 9]

z := transform([-3, 1, -5], abs)
println z // [3 1 5]

Lambda expressions

You also can use lambda expression to define a anonymous function.

func transform(a []float64, f func(float64) float64) []float64 {
    return [f(x) for x <- a]
}

y := transform([1, 2, 3], x => x*x)
println y // [1 4 9]

z := transform([-3, 1, -5], x => {
    if x < 0 {
        return -x
    }
    return x
})
println z // [3 1 5]

Structs

Custom iterators

For range of UDT

type Foo struct {
}

// Gop_Enum(proc func(val ValType)) or:
// Gop_Enum(proc func(key KeyType, val ValType))
func (p *Foo) Gop_Enum(proc func(key int, val string)) {
    // ...
}

foo := &Foo{}
for k, v := range foo {
    println k, v
}

for k, v <- foo {
    println k, v
}

println {v: k for k, v <- foo}

Note: you can't use break/continue or return statements in for range of udt.Gop_Enum(callback).

For range of UDT2

type FooIter struct {
}

// (Iterator) Next() (val ValType, ok bool) or:
// (Iterator) Next() (key KeyType, val ValType, ok bool)
func (p *FooIter) Next() (key int, val string, ok bool) {
    // ...
}

type Foo struct {
}

// Gop_Enum() Iterator
func (p *Foo) Gop_Enum() *FooIter {
    // ...
}

foo := &Foo{}
for k, v := range foo {
    println k, v
}

for k, v <- foo {
    println k, v
}

println {v: k for k, v <- foo}

Deduce struct type

type Config struct {
    Dir   string
    Level int
}

func foo(conf *Config) {
    // ...
}

foo {Dir: "/foo/bar", Level: 1}

Here foo {Dir: "/foo/bar", Level: 1} is equivalent to foo(&Config{Dir: "/foo/bar", Level: 1}). However, you can't replace foo(&Config{"/foo/bar", 1}) with foo {"/foo/bar", 1}, because it is confusing to consider {"/foo/bar", 1} as a struct literal.

You also can omit struct types in a return statement. For example:

type Result struct {
    Text string
}

func foo() *Result {
    return {Text: "Hi, Go+"} // return &Result{Text: "Hi, Go+"}
}

Overload operators

import "math/big"

type MyBigInt struct {
    *big.Int
}

func Int(v *big.Int) MyBigInt {
    return MyBigInt{v}
}

func (a MyBigInt) + (b MyBigInt) MyBigInt { // binary operator
    return MyBigInt{new(big.Int).Add(a.Int, b.Int)}
}

func (a MyBigInt) += (b MyBigInt) {
    a.Int.Add(a.Int, b.Int)
}

func -(a MyBigInt) MyBigInt { // unary operator
    return MyBigInt{new(big.Int).Neg(a.Int)}
}

a := Int(1r)
a += Int(2r)
println a + Int(3r)
println -a

Auto property

Let's see an example written in Go+:

import "gop/ast/goptest"

doc := goptest.New(`... Go+ code ...`)!

println doc.Any().FuncDecl().Name()

In many languages, there is a concept named property who has get and set methods.

Suppose we have get property, the above example will be:

import "gop/ast/goptest"

doc := goptest.New(`... Go+ code ...`)!

println doc.any.funcDecl.name

In Go+, we introduce a concept named auto property. It is a get property, but is implemented automatically. If we have a method named Bar(), then we will have a get property named bar at the same time.

Go/Go+ hybrid programming

This is an example to show how to mix Go/Go+ code in the same package.

In this example, we have a Go source file named a.go:

package main

import "fmt"

func p(a interface{}) {
    sayMix()
    fmt.Println("Hello,", a)
}

And we have a Go+ source file named b.gop:

func sayMix() {
    println "Mix Go and Go+"
}

p "world"

You can see that Go calls a Go+ function named sayMix, and Go+ calls a Go function named p. As you are used to in Go programming, this kind of circular reference is allowed.

Run gop run . to see the output of this example:

Mix Go and Go+
Hello, world

Run Go+ in watch mode

The gop command can run in watch mode so that everytime a Go+ file is changed it is transpiled to a Go file:

gop watch [-gentest] [dir]

By default gop watch does not convert test files (normally ending with _test.gop). You can specify -gentest flag to force converting all Go+ files.

Calling C from Go+

  • The gop c command (equivalent to the stand-alone c2go command) can be used to convert a C project to a Go project.
  • import "C" and import "C/xxx" are used to import a C project converted by c2go. where import "C" is short for import "C/github.com/goplus/libc".
  • The C"xxx" syntax represents C-style string constants.

Here is an example to show how Go+ interacts with C.

import "C"

C.printf C"Hello, c2go!\n"
C.fprintf C.stderr, C"Hi, %7.1f\n", 3.14

In this example we call two C standard functions printf and fprintf, passing a C variable stderr and two C strings in the form of C"xxx" (a Go+ syntax to represent C-style strings).

Run gop run . to see the output of this example:

Hello, c2go!
Hi,     3.1

Of course, the current Go+ support for C is only a preview version, not to the extent that it is actually available in engineering. As far as libc is concerned, the current migration progress is only about 5%, and it is just the beginning.

In the upcoming Go+ v1.2 version planning, complete support for C is listed as a top priority. Of course, support for cgo and Go templates is also under planning, which is a crucial capability enhancement for Go/Go+ hybrid projects.

Data processing

Rational numbers

We introduce rational numbers as primitive Go+ types. We use suffix r to denote rational literals. For example, 1r << 200 means a big int whose value is equal to 2200.

a := 1r << 200
b := bigint(1 << 200)

By default, 1r will have the type of bigint.

And 4/5r means the rational constant 4/5. It will have the type of bigrat.

a := 4/5r
b := a - 1/3r + 3 * 1/2r
println a, b // 4/5 59/30

Casting rational numbers works like other primitive types:

a := 1r
b := bigrat(1r)
c := bigrat(1)
println a/3 // 0
println b/3 // 1/3
println c/3 // 1/3

List comprehension

a := [x*x for x <- [1, 3, 5, 7, 11]]
b := [x*x for x <- [1, 3, 5, 7, 11] if x > 3]
c := [i+v for i, v <- [1, 3, 5, 7, 11] if i%2 == 1]

arr := [1, 2, 3, 4, 5, 6]
d := [[a, b] for a <- arr if a < b for b <- arr if b > 2]

x := {x: i for i, x <- [1, 3, 5, 7, 11]}
y := {x: i for i, x <- [1, 3, 5, 7, 11] if i%2 == 1}
z := {v: k for k, v <- {1: "Hello", 3: "Hi", 5: "xsw", 7: "Go+"} if k > 3}

Select data from a collection

type student struct {
    name  string
    score int
}

students := [student{"Ken", 90}, student{"Jason", 80}, student{"Lily", 85}]

unknownScore, ok := {x.score for x <- students if x.name == "Unknown"}
jasonScore := {x.score for x <- students if x.name == "Jason"}

println unknownScore, ok // 0 false
println jasonScore // 80

Check if data exists in a collection

type student struct {
    name  string
    score int
}

students := [student{"Ken", 90}, student{"Jason", 80}, student{"Lily", 85}]

hasJason := {for x <- students if x.name == "Jason"} // is any student named Jason?
hasFailed := {for x <- students if x.score < 60}     // is any student failed?

Unix shebang

You can use Go+ programs as shell scripts now. For example:

#!/usr/bin/env -S gop run

println "Hello, Go+"

println 1r << 129
println 1/3r + 2/7r*2

arr := [1, 3, 5, 7, 11, 13, 17, 19]
println arr
println [x*x for x <- arr, x > 3]

m := {"Hi": 1, "Go+": 2}
println m
println {v: k for k, v <- m}
println [k for k, _ <- m]
println [v for v <- m]

Compatibility with Go

All Go features will be supported (including partially support cgo, see below).

All Go packages (even these packages use cgo) can be imported by Go+.

import (
    "fmt"
    "strings"
)

x := strings.NewReplacer("?", "!").Replace("hello, world???")
fmt.Println "x:", x

And all Go+ packages can also be imported in Go programs. What you need to do is just using gop command instead of go.

First, let's make a directory named 14-Using-goplus-in-Go.

Then write a Go+ package named foo in it:

package foo

func ReverseMap(m map[string]int) map[int]string {
    return {v: k for k, v <- m}
}

Then use it in a Go package 14-Using-goplus-in-Go/gomain:

package main

import (
    "fmt"

    "github.com/goplus/tutorial/14-Using-goplus-in-Go/foo"
)

func main() {
    rmap := foo.ReverseMap(map[string]int{"Hi": 1, "Hello": 2})
    fmt.Println(rmap)
}

How to build this example? You can use:

gop install -v ./...

Go github.com/goplus/tutorial/14-Using-goplus-in-Go to get the source code.

Bytecode vs. Go code

Go+ supports bytecode backend and Go code generation.

When we use gop command, it generates Go code to covert Go+ package into Go packages.

gop run     # Run a Go+ program
gop install # Build Go+ files and install target to GOBIN
gop build   # Build Go+ files
gop test    # Test Go+ packages
gop fmt     # Format Go+ packages
gop clean   # Clean all Go+ auto generated files
gop go      # Convert Go+ packages into Go packages

When we use igop command, it generates bytecode to execute.

igop  # Run a Go+ program

In bytecode mode, Go+ doesn't support cgo. However, in Go-code-generation mode, Go+ fully supports cgo.