gno -> go
Making things in web3 with a new programming language, gno.

Making things in web3 with a new programming language, gno.
I’ve developed dozens of websites using Go and recently I’ve been drawn by my curiosity into how website/app development would be done with “web3” technologies. Luckily, there is a new language called “ Gno” that is lowering the barrier to entry for developing in web3.
Gno isn’t just another programming language; it’s an interpreted version of the Go language, optimized for the intricacies of blockchain and deterministic execution on distributed systems. If you’re familiar with Go, transitioning to Gno is a breeze.
Here are some key differentiators between Go and Gno:
avl.Tree
for a deterministic map.The differences may seem minute, but they pave the way for powerful blockchain applications with unparalleled precision.
There is a lot more information on their main website at gno.land. Also you can just easily play with Gno code online too, at the play.gno.land website.
“Smart contracts” are are immutable, self-executing programs living on the blockchain.
Beyond automating transactions, smart contracts can be used for various applications, such as creating incentivized social networks and reshaping interactions with the web (i.e., “web3”). Smart contracts written in Gno run within the Gno.land ecosystem.
Gno.land is a platform for writing smart contracts in Gno, the first of a series of Gno Layer 1 chains. Built on Tendermint2, Cosmos/IBC, and secured by Proof of Contribution, Gno.land prioritizes simplicity, security, scalability, and transparency.
For my first attempt into web3 I wanted to make a simple, shareable system of music. Music could be encoded into the blockchain and it could be easily recorded (verifiably) that some piece of music was generated first by some identity on the chain. I went for bytebeat music because its very straightforward to code. bytebeat was discovered by viznut in 2011 as a way to type a very short computer programs that generate chiptune music, for example this is a bytebeat program:
main(t){
for(;;t++) putchar(((t<<1)^((t<<1)+(t>>7)&t>>12)));
}
which you can take the raw output of and convert to audio:
gcc -o crowd crowd.c
./crowd | head -c 4M > crowd.raw
sox -r 8000 -c 1 -t u8 crowd.raw crowd.wav
The thing that amazed me about the Gno language is how fast it is to get started. First of all, the language is basically Go, plus a few addons. Also its mostly “batteries included” (persistent globals = database, getting callers = identification). Here is, essentially, my entire “smart contract” for generating bytebeat music, in just 50 lines of code:
package bytebeat
import (
"std"
bytebeat "gno.land/p/demo/audio/bytebeat/v1"
"gno.land/p/demo/ufmt"
)
type Comment struct {
User string
Message string
}
var (
data string
comments []Comment
)
func init() {
seconds := uint32(10)
data = bytebeat.ByteBeat(seconds, 8000, func(t int) int {
return (t>>10^t>>11)%5*((t>>14&3^t>>15&1)+1)*t%99 + ((3 + (t >> 14 & 3) - (t >> 16 & 1)) / 3 * t % 99 & 64)
})
}
func AddComment(s string) string {
caller := std.GetOrigCaller() // main
comments = append(comments, Comment{
User: string(caller),
Message: s,
})
return "ok"
}
func Render(path string) string {
output := ""
output += "\n"
output += "# my bytebeat\n"
output += `<audio controls="controls" autobuffer="autobuffer" autoplay="autoplay">
<source src="data:audio/wav;base64,`
output += data
output += `" />
</audio>`
output += "\n\n## comments\n"
for i, comment := range comments {
output += ufmt.Sprintf("%d. *%s* - %s\n ", i, comment.Message, comment.User)
}
return output
}
There are some clever built-in functions here, like the Render
function is automatically used by gno.land to do web rendering - so in addition to a smart contract you get a web server for free.
This smart contract is not yet totally “in action” yet because there is no main net to deliver it. But it is really straightforward and easy to setup your own Gno.land server to run smart contracts. I wrote a whole presentation on the topic which you can read more about here.
For another attempt into web3 I was interested in making a Twitter-like thing - essentially a way to “microblog”. Again, my smart contract for this package is extremely concise:
package microblog
import (
"std"
"gno.land/p/demo/microblog"
"gno.land/r/demo/users"
)
var (
title = "gno-based microblog"
prefix = "/r/demo/microblog:"
m *microblog.Microblog
)
func init() {
m = microblog.NewMicroblog(title, prefix)
}
// Render calls the microblog renderer
func Render(path string) string {
return m.Render(path)
}
// NewPost takes a single argument (post markdown) and
// adds a post to the address of the caller.
func NewPost(text string) string {
if err := m.NewPost(text); err != nil {
return "unable to add new post"
}
return "added new post"
}
func Register(name, profile string) string {
caller := std.GetOrigCaller() // main
users.Register(caller, name, profile)
return "OK"
}
A lot of work is being done behind the scenes here with some “packages”. There is a difference between “Packages” and “Realms” in Gno - where both are like Go packages in that they are modular and importable, but “Realms” have the special ability that they persist globals, as they are meant to be run on the block chain.
The package for the microblog is the most important and uses a lot of the cool Gno features. There is a AVL tree for managing data and some user management properties built into the language:
package microblog
import (
"errors"
"sort"
"std"
"strings"
"time"
"gno.land/p/demo/avl"
"gno.land/p/demo/ufmt"
"gno.land/r/demo/users"
)
var (
ErrNotFound = errors.New("not found")
StatusNotFound = "404"
)
type Microblog struct {
Title string
Prefix string // i.e. r/gnoland/blog:
Pages avl.Tree // author (string) -> Page
}
func NewMicroblog(title string, prefix string) (m *Microblog) {
return &Microblog{
Title: title,
Prefix: prefix,
Pages: avl.Tree{},
}
}
func (m *Microblog) GetPages() []*Page {
var (
pages = make([]*Page, m.Pages.Size())
index = 0
)
m.Pages.Iterate("", "", func(key string, value interface{}) bool {
pages[index] = value.(*Page)
index++
return false
})
sort.Sort(byLastPosted(pages))
return pages
}
func (m *Microblog) RenderHome() string {
output := ufmt.Sprintf("# %s\n\n", m.Title)
output += "# pages\n\n"
for _, page := range m.GetPages() {
if u := users.GetUserByAddress(page.Author); u != nil {
output += ufmt.Sprintf("- [%s (%s)](%s%s)\n", u.Name(), page.Author.String(), m.Prefix, page.Author.String())
} else {
output += ufmt.Sprintf("- [%s](%s%s)\n", page.Author.String(), m.Prefix, page.Author.String())
}
}
return output
}
func (m *Microblog) RenderUser(user string) string {
silo, found := m.Pages.Get(user)
if !found {
return StatusNotFound
}
return (silo.(*Page)).String()
}
func (m *Microblog) Render(path string) string {
parts := strings.Split(path, "/")
isHome := path == ""
isUser := len(parts) == 1
switch {
case isHome:
return m.RenderHome()
case isUser:
return m.RenderUser(parts[0])
}
return StatusNotFound
}
func (m *Microblog) NewPost(text string) error {
author := std.GetOrigCaller()
_, found := m.Pages.Get(author.String())
if !found {
// make a new page for the new author
m.Pages.Set(author.String(), &Page{
Author: author,
CreatedAt: time.Now(),
})
}
page, err := m.GetPage(author.String())
if err != nil {
return err
}
return page.NewPost(text)
}
func (m *Microblog) GetPage(author string) (*Page, error) {
silo, found := m.Pages.Get(author)
if !found {
return nil, ErrNotFound
}
return silo.(*Page), nil
}
type Page struct {
ID int
Author std.Address
CreatedAt time.Time
LastPosted time.Time
Posts avl.Tree // time -> Post
}
// byLastPosted implements sort.Interface for []Page based on
// the LastPosted field.
type byLastPosted []*Page
func (a byLastPosted) Len() int { return len(a) }
func (a byLastPosted) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byLastPosted) Less(i, j int) bool { return a[i].LastPosted.After(a[j].LastPosted) }
func (p *Page) String() string {
o := ""
if u := users.GetUserByAddress(p.Author); u != nil {
o += ufmt.Sprintf("# [%s](/r/demo/users:%s)\n\n", u.Name(), u.Name())
o += ufmt.Sprintf("%s\n\n", u.Profile())
}
o += ufmt.Sprintf("## [%s](/r/demo/microblog:%s)\n\n", p.Author, p.Author)
o += ufmt.Sprintf("joined %s, last updated %s\n\n", p.CreatedAt.Format("2006-02-01"), p.LastPosted.Format("2006-02-01"))
o += "## feed\n\n"
for _, u := range p.GetPosts() {
o += u.String() + "\n\n"
}
return o
}
func (p *Page) NewPost(text string) error {
now := time.Now()
p.LastPosted = now
p.Posts.Set(ufmt.Sprintf("%s%d", now.Format(time.RFC3339), p.Posts.Size()), &Post{
ID: p.Posts.Size(),
Text: text,
CreatedAt: now,
})
return nil
}
func (p *Page) GetPosts() []*Post {
posts := make([]*Post, p.Posts.Size())
i := 0
p.Posts.ReverseIterate("", "", func(key string, value interface{}) bool {
postParsed := value.(*Post)
posts[i] = postParsed
i++
return false
})
return posts
}
// Post lists the specific update
type Post struct {
ID int
CreatedAt time.Time
Text string
}
func (p *Post) String() string {
return "> " + strings.ReplaceAll(p.Text, "\n", "\n>\n>") + "\n>\n> *" + p.CreatedAt.Format(time.RFC1123) + "*"
}
Its a bit more code, but having a full-featured microblog with users and posts and persistent data, with only a Gno standard library is pretty amazing to me.
There is a lot more information about this project that you can dive into here where you can also read aobut how to spin up your own server.
Gno provides Go developers with a smooth transition into the world of smart contracts. With a syntax close to Go and a platform like Gno.land, developing decentralized applications becomes accessible. While Gno introduces some unique features and limitations, its potential for web3 development is promising.