Proposal: Go 2: add chained interval comparisons

6 min read Original article ↗

Python comparisons can be chained like:

This means:

  • x is evaluated
  • y is evaluated
  • if x < y then z is evaluated, else skip master if body
  • if y <= z then flow proceeds into master if body

This is intuitive and easier to read. This proposal covers monotone relations only (<,<=,>,>= in one direction) like:

  • x < y <= z
  • a > b > c

It does not cover any non-monotone relations like:

  • p >= q < r
  • q < w != e
  • a != s == d

In order to determine places where such chained interval comparisons could be used in Go, we can use an (improved) bash script like:

# Finds 3 component monotone comparisons
name='( [a-zA-Z_][a-zA-Z_0-9.*/+%()-]* )'
nmrf='(?:\1|\2)'
less='<=?'
more='>=?'
keyw='(?:if|for|case).*'
logi='[^|&]*(?:&&|\|\|)[^|&]*'

patl=(
"$keyw(?:$less$name|$name$more)$logi(?:$nmrf$less|$more$nmrf)"

"$keyw(?:$more$name|$name$less)$logi(?:$nmrf$more|$less$nmrf)"
)
s=0
for pat in "${patl[@]}"; do
	r=$(grep -Pr --include '*.go' "$pat" . | grep -c .)
	let s+=r
	grep -Prm 1 --include '*.go' "$pat" . | head -n 3
done
echo "$s+ cases"

On some popular projects developed in Go, we get the following examples & totals:

examples from dgraph:

./chunker/json_parser.go:		if buf.batchSize > 0 && len(buf.nquads) >= buf.batchSize {
./chunker/rdf_state.go:	case r >= 'a' && r <= 'z':
./gql/parser.go:		if depth > 4 || depth < 0 {
./compose/compose.go:	if opts.NumZeros < 1 || opts.NumZeros > 99 {
./dgraph/cmd/zero/tablet.go:			if tab.Space <= sizeDiff/2 && tab.Space > size {
./lex/lexer.go:	if p.idx < 0 || p.idx >= len(p.l.items) {

448+ cases

examples from etcd:

./client/client_test.go:	if ratio := float64(pinNum) / float64(round); ratio > max || ratio < min {
./clientv3/client.go:		if cfg.MaxCallRecvMsgSize > 0 && cfg.MaxCallSendMsgSize > cfg.MaxCallRecvMsgSize {
./functional/runner/global.go:			for rc.progress < rounds || rounds <= 0 {
./auth/store.go:	if bcryptCost < bcrypt.MinCost || bcryptCost > bcrypt.MaxCost {
./integration/v3_lease_test.go:	if ttlresp.TTL < expectedTTL-1 || ttlresp.TTL > expectedTTL {
./mvcc/kvstore_txn.go:	if limit <= 0 || limit > len(revpairs) {

276+ cases

examples from frp:

./vendor/github.com/fatedier/beego/logs/logger.go:	case code >= 200 && code < 300:
./vendor/github.com/fatedier/kcp-go/kcp.go:	if n >= int(kcp.mtu-IKCP_OVERHEAD) || n < 0 {
./vendor/github.com/gorilla/websocket/conn.go:		if c.readLimit > 0 && c.readLength > c.readLimit {
./vendor/github.com/golang/snappy/decode_other.go:		if offset <= 0 || d < offset || length > len(dst)-d {
./vendor/github.com/gorilla/websocket/x_net_proxy.go:	if port < 1 || port > 0xffff {
./vendor/github.com/hashicorp/yamux/session.go:		if mt < typeData || mt > typeGoAway {

219+ cases

examples from gitea:

./models/git_diff.go:			if begin <= line && end >= line {
./modules/auth/auth.go:	if token.ExpiresAt < time.Now().Unix() || token.IssuedAt > time.Now().Unix() {
./modules/git/signature.go:		if firstChar >= 48 && firstChar <= 57 {
./models/action.go:		if slashIndex < 0 || slashIndex >= poundIndex {
./models/repo_collaboration.go:	if mode <= AccessModeNone || mode > AccessModeOwner {
./modules/indexer/repo.go:			if startIndex < 0 || locationStart < startIndex {

590+ cases

examples from go:

./misc/cgo/testshared/shared_test.go:			if prog.Off <= offset && offset < prog.Off+prog.Filesz {
./src/archive/tar/format.go:		if 148 <= i && i < 156 {
./src/bufio/bufio.go:		if n > 0 && n < b.n {
./misc/cgo/gmp/gmp.go:	if base < 2 || base > 36 {
./src/archive/tar/strconv.go:	if perr != nil || n < 5 || int64(len(s)) < n {
./src/archive/zip/reader.go:	if o := int64(d.directoryOffset); o < 0 || o >= size {

1369+ cases

examples from influxdb:

./bolt/bucket.go:		if limit > 0 && len(bs) >= limit {
./bolt/dashboard.go:		if limit > 0 && len(ds) >= limit {
./chronograf/oauth2/cookies.go:	if lifespan > 0 && inactivity > lifespan {
./chronograf/influx/queries/select.go:		if got := len(v.Args); got < 1 || got > 2 {
./chronograf/oauth2/mux_test.go:	if resp.StatusCode < 300 || resp.StatusCode >= 400 {
./cmd/influx/task.go:	if taskFindFlags.limit < 1 || taskFindFlags.limit > platform.TaskMaxPageSize {

53+ cases

examples from kubernetes:

./cmd/kubeadm/app/preflight/checks.go:			if r != nil && r.StatusCode >= 500 && r.StatusCode <= 599 {
./cmd/kubeadm/app/util/endpoint.go:	if err == nil && (1 <= portInt && portInt <= 65535) {
./cmd/kubeadm/app/util/system/package_validator.go:			case c >= '0' && c <= '9':
./cmd/kube-apiserver/app/options/validation.go:	if options.KubernetesServiceNodePort < 0 || options.KubernetesServiceNodePort > 65535 {
./cmd/kube-scheduler/app/options/insecure_serving.go:	if o.BindPort < 0 || o.BindPort > 65535 {
./pkg/apis/core/validation/validation.go:	if iscsi.Lun < 0 || iscsi.Lun > 255 {

1524+ cases

examples from moby:

./client/request.go:	if serverResp.statusCode >= 200 && serverResp.statusCode < 400 {
./daemon/daemon_unix.go:	if resources.Memory > 0 && resources.MemorySwap > 0 && resources.MemorySwap < resources.Memory {
./daemon/events/events.go:		if untilNanoUnix > 0 && ev.TimeNano > untilNanoUnix {
./builder/dockerfile/evaluator.go:	if i < 0 || i > len(r.flat) {
./builder/remotecontext/remote.go:	if plen <= 0 || plen > maxPreambleLength {
./daemon/cluster/listen_addr.go:	if portNum < 1024 || portNum > 49151 {

461+ cases

examples from nomad:

./client/fs_endpoint.go:	if limit > 0 && limit < streamFrameSize {
./command/agent/alloc_endpoint.go:	if len(tokens) > 2 || len(tokens) < 1 {
./command/agent/retry_join.go:		if serverJoin.RetryMaxAttempts > 0 && attempt > serverJoin.RetryMaxAttempts {
./api/internal/testutil/freeport/freeport.go:		if port < firstPort+1 || port >= firstPort+blockSize {
./client/stats/cpu_test.go:	if percent < expectedPercent && percent > (expectedPercent+1.00) {
./command/agent/config.go:	if 0 > port || port > 65535 {

769+ cases

examples from prometheus:

./documentation/examples/remote_storage/remote_storage_adapter/opentsdb/tagvalue.go:			case b >= '0' && b <= '9':
./pkg/textparse/openmetricslex.l.go:	case c == ':' || c >= 'A' && c <= 'Z' || c == '_' || c >= 'a' && c <= 'z':
./pkg/textparse/promlex.l.go:	case c == ':' || c >= 'A' && c <= 'Z' || c == '_' || c >= 'a' && c <= 'z':
./cmd/prometheus/main.go:					if cfg.tsdb.WALSegmentSize < 10*1024*1024 || cfg.tsdb.WALSegmentSize > 256*1024*1024 {
./pkg/textparse/openmetricslex.l.go:	case c >= '0' && c <= ':' || c >= 'A' && c <= 'Z' || c == '_' || c >= 'a' && c <= 'z':
./pkg/textparse/promlex.l.go:	case c >= '0' && c <= ':' || c >= 'A' && c <= 'Z' || c == '_' || c >= 'a' && c <= 'z':

550+ cases

examples from terraform:

./config/interpolate_funcs.go:				if i >= from && i < to {
./configs/variabletypehint_string.go:	case 76 <= i && i <= 77:
./helper/resource/state.go:			if conf.PollInterval > 0 && conf.PollInterval < 180*time.Second {
./config/resource_mode_string.go:	if i < 0 || i >= ResourceMode(len(_ResourceMode_index)-1) {
./configs/configschema/internal_validate.go:			case blockS.MinItems < 0 || blockS.MinItems > 1:
./configs/configschema/nestingmode_string.go:	if i < 0 || i >= NestingMode(len(_NestingMode_index)-1) {

653+ cases

As seen from 6912+ cases above, many thousands of if / case / for clauses doing interval checks can be made simpler and easier to read. Also, it has advantages for IEEE floats, see below.
What do you think?