v0.7.4 beta
Back-end UI Framework

for feature-rich, secure, and fast web apps in Go

Tutorial

Integrations

Using a database, calling an external API, or generating SVG isn’t the main focus of this tutorial. I recommend just copying and pasting those snippets.

1. Database

To search for countries and cities, I used SQLite databases from here

./sqlite
├── cities.sqlite3
└── countries.sqlite3

To use them, install the SQLite library:

 go get github.com/mattn/go-sqlite3

./driver/countries.go

package driver

import (
	"database/sql"
	"errors"
)

type CountriesDb struct {
	db *sql.DB
}

func (d *CountriesDb) Get(id int) (Place, error) {
	var p Place
	query := `
		SELECT id, name
		FROM countries
		WHERE id = ?
		LIMIT 1
	`
	row := d.db.QueryRow(query, id)
	if err := row.Scan(&p.Id, &p.Name); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return Place{}, nil
		}
		return Place{}, err
	}
	return p, nil
}

func (d *CountriesDb) Search(term string) ([]Place, error) {
	query := `
		SELECT id, name
		FROM countries
		WHERE LOWER(name) LIKE LOWER(?)
		LIMIT 7
	`
	rows, err := d.db.Query(query, term+"%")
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var results []Place
	for rows.Next() {
		var c Place
		if err := rows.Scan(&c.Id, &c.Name); err != nil {
			return nil, err
		}
		results = append(results, c)
	}
	return results, nil
}

./driver/cities.go

package driver

import (
	"database/sql"
	"errors"
)

type CitiesDb struct {
	db *sql.DB
}

type City struct {
	Name    string
	Country Place
	Id      int
	Lat     float64
	Long    float64
}

func (d *CitiesDb) Get(city int) (City, error) {
	var c City
	row := d.db.QueryRow(`
		SELECT id, name, latitude, longitude, country_id
		FROM cities
		WHERE id = ?
		LIMIT 1
	`, city)

	var countryId int
	var err error
	if err = row.Scan(&c.Id, &c.Name, &c.Lat, &c.Long, &countryId); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return City{}, nil
		}
		return City{}, err
	}
	c.Country, err = Countries.Get(countryId)
	if err != nil {
		return City{}, err
	}
	return c, nil
}

func (d *CitiesDb) Search(country int, term string) ([]Place, error) {
	query := `
		SELECT id, name
		FROM cities
		WHERE country_id = ?
		AND LOWER(name) LIKE LOWER(?)
		LIMIT 7
	`
	rows, err := d.db.Query(query, country, term+"%")
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var results []Place
	for rows.Next() {
		var c Place
		if err := rows.Scan(&c.Id, &c.Name); err != nil {
			return nil, err
		}
		results = append(results, c)
	}
	return results, nil
}

2. Weather API

Connect a weather API to retrieve time-series data for the dashboard charts.

./driver/weather.go

package driver

import (
	"context"
	"encoding/json"
	"net/http"
	"time"

	"fmt"
)

type WeatherAPI struct {
	endpoint string
	timeout  time.Duration
}


func (w *WeatherAPI) parseTime(str string) (time.Time, error) {
	layout := "2006-01-02T15:04"
	return time.Parse(layout, str)
}

func (w *WeatherAPI) request(ctx context.Context, city City, parameter parameter, units Units, days int) (Response, error) {
	ctx, cancel := context.WithTimeout(ctx, w.timeout)
	defer cancel()
	url := fmt.Sprintf(
		"%s?latitude=%.2f&longitude=%.2f%s%s&forecast_days=%d",
		w.endpoint,
		city.Lat, city.Long,
		parameter.param(), units.param(),
		days,
	)
	var r Response
	res, err := http.Get(url)
	if err != nil {
		return r, err
	}
	defer res.Body.Close()
	err = json.NewDecoder(res.Body).Decode(&r)
	if err != nil {
		return r, err
	}
	for i, v := range r.Hourly.Time {
		t, err := w.parseTime(v)
		if err != nil {
			return r, err
		}
		if days < 3 {
			r.Hourly.Time[i] = t.Format("15:04") + " " 
		} else {
			r.Hourly.Time[i] = t.Format("02.01")
		}
	}
	return r, err
}

type FloatSamples struct {
	Labels []string  `json:"labels"`
	Values []float64 `json:"values"`
}

type StringSamples struct {
	Labels []string `json:"labels"`
	Values []string `json:"values"`
}

func (w *WeatherAPI) Humidity(ctx context.Context, city City, days int) (FloatSamples, error) {
	r, err := w.request(ctx, city, humidity, noUnits, days)
	if err != nil {
		return FloatSamples{}, err
	}
	samples := FloatSamples{
		Labels: make([]string, len(r.Hourly.Time)),
		Values: make([]float64, len(r.Hourly.Time)),
	}
	for i := range r.Hourly.Time {
		samples.Labels[i] = r.Hourly.Time[i]
		samples.Values[i] = r.Hourly.RelativeHumidity2m[i]
	}
	return samples, nil
}

func (w *WeatherAPI) Temperature(ctx context.Context, city City, units Units, days int) (FloatSamples, error) {
	r, err := w.request(ctx, city, temperature, units, days)
	if err != nil {
		return FloatSamples{}, err
	}
	samples := FloatSamples{
		Labels: make([]string, len(r.Hourly.Time)),
		Values: make([]float64, len(r.Hourly.Time)),
	}
	for i := range r.Hourly.Time {
		samples.Labels[i] = r.Hourly.Time[i]
		samples.Values[i] = r.Hourly.Temperature2m[i]
	}
	return samples, nil
}

func (w *WeatherAPI) WindSpeed(ctx context.Context, city City, units Units, days int) (FloatSamples, error) {
	r, err := w.request(ctx, city, windSpeed, noUnits, days)
	if err != nil {
		return FloatSamples{}, err
	}
	samples := FloatSamples{
		Labels: make([]string, len(r.Hourly.Time)),
		Values: make([]float64, len(r.Hourly.Time)),
	}
	for i := range r.Hourly.Time {
		samples.Labels[i] = r.Hourly.Time[i]
		samples.Values[i] = r.Hourly.WindSpeed10m[i]
	}
	return samples, nil
}

func (w *WeatherAPI) Code(ctx context.Context, city City, days int) (StringSamples, error) {
	r, err := w.request(ctx, city, weatherCode, Metric, days)
	if err != nil {
		return StringSamples{}, err
	}
	samples := StringSamples{
		Labels: make([]string, len(r.Hourly.Time)),
		Values: make([]string, len(r.Hourly.Time)),
	}
	for i := range r.Hourly.Time {
		samples.Labels[i] = r.Hourly.Time[i]
		str, ok := weatherCodeShort[r.Hourly.WeatherCode[i]]
		if !ok {
			str = "unknown"
		}
		samples.Values[i] = str
	}
	return samples, nil
}

var weatherCodeShort = map[int]string{
	0:  "Clear",
	1:  "Mainly clear",
	2:  "Partly cloudy",
	3:  "Overcast",
	45: "Fog",
	48: "Rime fog",
	51: "Drizzle light",
	53: "Drizzle mod",
	55: "Drizzle dense",
	56: "Frzg drizzle lgt",
	57: "Frzg drizzle hvy",
	61: "Rain light",
	63: "Rain mod",
	65: "Rain heavy",
	66: "Frzg rain lgt",
	67: "Frzg rain hvy",
	71: "Snow light",
	73: "Snow mod",
	75: "Snow heavy",
	77: "Snow grains",
	80: "Shower rain lgt",
	81: "Shower rain mod",
	82: "Shower rain hvy",
	85: "Snow shower lgt",
	86: "Snow shower hvy",
	95: "Thunderstorm",
	96: "Storm + small hail",
	99: "Storm + heavy hail",
}

type Units int

const (
	Metric Units = iota
	Imperial
	noUnits
)

func (u Units) Label() string {
	if u == Imperial {
		return "Imperial"
	}
	if u == Metric {
		return "Metric"
	}
	return "unknown"
}


func (u Units) WindSpeed() string {
	if u == Imperial {
		return "KMH"
	}
	if u == Metric {
		return "MPH"
	}
	return "unknown"
}

func (u Units) Temperature() string {
	if u == Imperial {
		return " °F"
	}
	if u == Metric {
		return " °C"
	}
	return "unknown"
}

func (u Units) Humidity() string {
	return "%"
}

func (u Units) Ref() *Units {
	return &u
}

func (u Units) param() string {
	if u == Metric {
		return "&wind_speed_unit=kmh&temperature_unit=celsius&precipitation_unit=mm"
	}
	if u == Imperial {
		return "&wind_speed_unit=mph&temperature_unit=fahrenheit&precipitation_unit=inch"
	}
	return ""
}

type parameter string

const (
	temperature parameter = "temperature_2m"
	humidity    parameter = "relative_humidity_2m"
	windSpeed   parameter = "wind_speed_10m"
	weatherCode parameter = "weather_code"
)

func (u parameter) param() string {
	return "&hourly=" + string(u)
}

type Response struct {
	Hourly struct {
		Time               []string  `json:"time"`
		Temperature2m      []float64 `json:"temperature_2m"`
		RelativeHumidity2m []float64 `json:"relative_humidity_2m"`
		WindSpeed10m       []float64 `json:"wind_speed_10m"`
		Rain               []float64 `json:"rain"`
		WeatherCode        []int     `json:"weather_code"`
	} `json:"hourly"`
}

3. Charts SVG

I used github.com/vicanso/go-charts to generate SVG based on the weather data.

 go get github.com/vicanso/go-charts

./driver/charts.go

package driver

import (
	"sort"

	"github.com/vicanso/go-charts/v2"
)

func ChartLine(values []float64, labels []string, unit string) ([]byte, error) {
	p, err := charts.LineRender(
		[][]float64{values},
		charts.SVGTypeOption(),
		func(opt *charts.ChartOption) {
			chartDefaults(opt)
			opt.XAxis = charts.NewXAxisOption(labels)
			opt.SymbolShow = charts.FalseFlag()
			opt.Legend = charts.LegendOption{
				Data: []string{unit},
			}
			opt.LineStrokeWidth = 2
		},
	)
	if err != nil {
		return nil, err
	}
	return p.Bytes()

}

func ChartPie(values []string) ([]byte, error) {
	m := make(map[string]float64)
	for _, v := range values {
		c, _ := m[v]
		m[v] = c + 1
	}
	counts := make([]float64, len(m))
	labels := make([]string, len(m))
	i := 0
	for k := range m {
		labels[i] = k
		i += 1
	}
	sort.Strings(labels)
	for i, label := range labels {
		counts[i] = m[label]
	}
	p, err := charts.PieRender(
		counts,
		charts.SVGTypeOption(),
		charts.PieSeriesShowLabel(),
		func(opt *charts.ChartOption) {
			chartDefaults(opt)
			f := false
			opt.Legend = charts.LegendOption{
				Orient: charts.OrientVertical,
				Data:   labels,
				Show:   &f,
			}
		},
	)
	if err != nil {
		return nil, err
	}
	return p.Bytes()
}

func chartDefaults(opt *charts.ChartOption) {
	opt.Theme = "dark"
	opt.Height = 400
	opt.BackgroundColor = charts.Color{
		R: 24,
		G: 28,
		B: 37,
		A: 255,
	}
}

github.com/vicanso/go-charts is good enough for tutorial, but it’s buggy and no longer maintained.

4. Init

Initialize wrappers for external use.

./driver/driver.go

package driver

import (
	"database/sql"
	"log"
	"time"

	_ "github.com/mattn/go-sqlite3"
)

type Place struct {
	Id   int
	Name string
}

var Countries *CountriesDb
var Cities *CitiesDb
var Weather *WeatherAPI

func init() {
	countries, err := sql.Open("sqlite3", "./sqlite/countries.sqlite3")
	if err != nil {
		log.Fatal("Failed to open database:", err)
	}
	Countries = &CountriesDb{db: countries}
	cities, err := sql.Open("sqlite3", "./sqlite/cities.sqlite3")
	if err != nil {
		log.Fatal("Failed to open database:", err)
	}
	Cities = &CitiesDb{db: cities}
	Weather = &WeatherAPI{
		endpoint: "https://api.open-meteo.com/v1/forecast",
		timeout:  10 * time.Second,
	}
}

Next: Location Selector