Created
April 12, 2023 17:15
-
-
Save mohamedattahri/8cacc47b710eb3e2b170eef59bcd4a5b to your computer and use it in GitHub Desktop.
Simple way to check if running the latest version of Go.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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