Skip to content

Instantly share code, notes, and snippets.

@mohamedattahri
Created April 12, 2023 17:15
Show Gist options
  • Save mohamedattahri/8cacc47b710eb3e2b170eef59bcd4a5b to your computer and use it in GitHub Desktop.
Save mohamedattahri/8cacc47b710eb3e2b170eef59bcd4a5b to your computer and use it in GitHub Desktop.
Simple way to check if running the latest version of Go.
package main
import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"log"
"net/http"
"os"
"runtime"
"strconv"
"strings"
"time"
)
const (
refURL = "https://go.dev/dl/?mode=json"
tipURL = "https://tip.golang.org/doc"
)
// errUpgrade is returned when
// a version of go is not the latest available.
type errUpgrade struct {
Current, Latest *goVer
}
// Error implements the error interface.
func (err *errUpgrade) Error() string {
return fmt.Sprintf("Go %s is available (%s/go%s)", err.Latest.Version(), tipURL, err.Latest.Release())
}
// errUnsupported is returned when
// a version of Go is no longer supported.
type errUnsupported struct {
Current *goVer
}
// Error implements the error interface.
func (err *errUnsupported) Error() string {
return fmt.Sprintf("Go %s is no longer supported", err.Current.Release())
}
// goVer represents a Go version.
type goVer struct {
Major, Minor, Revision int
}
// Release returns "{major}.{minor}".
func (v *goVer) Release() string {
return fmt.Sprintf("%d.%d", v.Major, v.Minor)
}
// Long returns "go{major}.{minor}.{revision} {os}/{arch}"
func (v *goVer) Long() string {
return fmt.Sprintf("%s %s/%s", v.String(), runtime.GOOS, runtime.GOARCH)
}
// Version returns "{major}.{minor}.{revision}" if
// revision > 0, "{major}.{minor}" otherwise.
func (v *goVer) Version() string {
str := v.Release()
if v.Revision > 0 {
str += fmt.Sprintf(".%d", v.Revision)
}
return str
}
// String returns a canonical string representation
// of goVer.
func (v *goVer) String() string {
return fmt.Sprintf("go%s", v.Version())
}
// UnmarshalJSON parses a Go version
// from a JSON string.
func (v *goVer) UnmarshalJSON(b []byte) (err error) {
var goVer string
err = json.Unmarshal(b, &goVer)
if err != nil {
return
}
parsed, err := parseGoVer(goVer)
if err != nil {
return
}
*v = *parsed
return nil
}
// parseGoVer parses a goVer from a go version string
// e.g. ^(go)?1.19.8(.*)?$
func parseGoVer(ver string) (v *goVer, err error) {
ver = strings.TrimSpace(ver)
if strings.HasPrefix(ver, "go") {
ver = ver[2:]
}
components := strings.Split(ver, ".")
if len(components) < 2 {
err = fmt.Errorf("invalid goVer representation: %v", ver)
return
}
v = new(goVer)
if v.Major, err = strconv.Atoi(components[0]); err != nil {
err = fmt.Errorf("invalid major component: %v", ver)
return
}
if v.Minor, err = strconv.Atoi(components[1]); err != nil {
err = fmt.Errorf("invalid minor component: %v", ver)
return
}
if len(components) > 2 {
if v.Revision, err = strconv.Atoi(components[2]); err != nil {
err = fmt.Errorf("invalid revision component: %v", ver)
return
}
}
return
}
// release represents a major release of Go.
type release struct {
Version *goVer `json:"version"`
Stable bool `json:"stable"`
Files []*struct {
OS string `json:"os"`
Arch string `json:"arch"`
Version *goVer `json:"version"`
} `json:"files"`
}
// fetch downloads the list of releases from go.dev.
func fetch(ctx context.Context) ([]*release, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, refURL, nil)
if err != nil {
return nil, fmt.Errorf("unable to create request: %v", err)
}
req.Header.Set("User-Agent", "cmd/go/version/"+runtime.Version())
req.Header.Set("Accept", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("server is not reachable: %v", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected response status code: %v", resp.StatusCode)
}
defer func() {
if err := resp.Body.Close(); err != nil {
log.Println("unable to close response body", err)
}
}()
results := make([]*release, 0)
if err := json.NewDecoder(resp.Body).Decode(&results); err != nil {
return nil, fmt.Errorf("unable to decode JSON response: %v", err)
}
return results, nil
}
// check returns an error if cur is not the latest revision.
func check(cur *goVer, releases []*release) error {
for _, r := range releases {
if !r.Stable {
continue
}
if r.Version.Major == cur.Major && r.Version.Minor == cur.Minor {
if r.Version.Revision > cur.Revision {
return &errUpgrade{Current: cur, Latest: r.Version}
}
return nil
}
}
return &errUnsupported{Current: cur}
}
// checkLatest returns errUpgrade if cur is not the latest
// version of Go available.
func checkLatest(cur *goVer, releases []*release) error {
cur, err := parseGoVer(runtime.Version())
if err != nil {
return err
}
var latest *goVer
for _, r := range releases {
if r.Stable {
latest = r.Version
}
}
if latest == nil {
return errors.New("unable to find a stable version in the list of releases")
}
if latest.Major > cur.Major ||
latest.Minor > cur.Minor ||
latest.Revision > cur.Minor {
return &errUpgrade{Current: cur, Latest: latest}
}
return nil
}
func checkCmd(latest bool) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
releases, err := fetch(ctx)
if err != nil {
fmt.Println(err)
os.Exit(1)
return
}
cur, err := parseGoVer(runtime.Version())
if err != nil {
panic(err)
}
var checkFunc func(*goVer, []*release) error
if latest {
checkFunc = checkLatest
} else {
checkFunc = check
}
if err := checkFunc(cur, releases); err != nil {
fmt.Println(err.Error())
os.Exit(1)
return
}
fmt.Printf("go version %s\n", cur.Long())
os.Exit(0)
}
func main() {
latest := flag.Bool("latest", false, "Check if running the latest major release")
flag.Parse()
checkCmd(*latest)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment