Repository: go
Issue: proposal: bytes, strings: add CutByte
Labels: Proposal

### Proposal Details

### Abstract

This proposal suggests the addition of a new function, `CutByte`, to the `strings` and `bytes` packages in the Go standard library. The function aims to simplify the handling of strings and byte slices by cutting them around the first instance of a specified byte separator. This function is designed to offer a more efficient alternative to the existing `Cut` function when the separator is a single byte, providing up to a 25% performance improvement in typical use cases.

### Background

The existing `Cut` function in the Go standard library has proven to be extremely useful for handling strings and byte slices by simplifying code and replacing multiple standard library functions. However, in scenarios where the separator is known to be a single byte, the `Cut` function can be optimized further. The proposed `CutByte` function addresses this by focusing on byte-level operations, which are common in many real-world applications, such as parsing binary protocols or handling ASCII-based text formats.

<details><summary>Details</summary>
<p>

```
./cmd/vet/vet_test.go:			if _, suffix, ok := strings.Cut(text, " "); ok {
./cmd/go/scriptreadme_test.go:	_, lang, ok := strings.Cut(doc.String(), "# Script Language\n\n")
./cmd/go/scriptreadme_test.go:	lang, _, ok = strings.Cut(lang, "\n\nvar ")
./cmd/go/internal/test/test.go:		op, name, found := strings.Cut(s, " ")
./cmd/go/internal/vcweb/script.go:			mPath, version, ok := strings.Cut(args[1], "@")
./cmd/go/internal/modfetch/fetch.go:				_, vers, _ = strings.Cut(vers, "-")
./cmd/go/internal/modfetch/fetch.go:					goos, goarch, _ := strings.Cut(vers[i+1:], "-")
./cmd/go/internal/modfetch/codehost/vcs.go:		before, after, found := strings.Cut(line, ":")
./cmd/go/internal/modfetch/toolchain.go:	_, v, ok := strings.Cut(v, "-")
./cmd/go/internal/modload/list.go:		if path, vers, found := strings.Cut(arg, "@"); found {
./cmd/go/internal/modload/list.go:		if path, vers, found := strings.Cut(arg, "@"); found {
./cmd/go/internal/modload/build.go:	if path, vers, found := strings.Cut(path, "@"); found {
./cmd/go/internal/modload/search.go:			elem, rest, found := strings.Cut(p, string(filepath.Separator))
./cmd/go/internal/modload/init.go:	line, _, _ := strings.Cut(string(buf[:n]), "\n")
./cmd/go/internal/envcmd/env.go:		key, val, found := strings.Cut(arg, "=")
./cmd/go/internal/modindex/build.go:		line, argstr, ok := strings.Cut(strings.TrimSpace(line[4:]), ":")
./cmd/go/internal/modindex/build.go:	name, _, _ = strings.Cut(name, ".")
./cmd/go/internal/modindex/build_read.go:			path, _, ok = strings.Cut(args[1:], "`")
./cmd/go/internal/script/engine.go:		prefix, suffix, ok := strings.Cut(cond.tag, ":")
./cmd/go/internal/script/engine.go:		if prefix, suffix, ok := strings.Cut(tag, ":"); ok {
./cmd/go/internal/script/state.go:		if k, v, ok := strings.Cut(kv, "="); ok {
./cmd/go/internal/modcmd/edit.go:	before, after, found := strings.Cut(arg, "@")
./cmd/go/internal/modcmd/edit.go:	before, after, found := strings.Cut(arg, "@")
./cmd/go/internal/modcmd/edit.go:	before, after, found := strings.Cut(s, ",")
./cmd/go/internal/modcmd/edit.go:	before, after, found := strings.Cut(arg, "=")
./cmd/go/internal/load/godebug.go:	k, v, ok := strings.Cut(strings.TrimSpace(text[i:]), "=")
./cmd/go/internal/vcs/vcs.go:		pattern, list, found := strings.Cut(item, ":")
./cmd/go/internal/vcs/vcs.go:	if scheme, path, ok := strings.Cut(repo, "://"); ok {
./cmd/go/internal/work/gccgo.go:		before, after, _ := strings.Cut(args, "=")
./cmd/go/internal/gover/gomod.go:	s, _, _ = strings.Cut(s, "//") // strip comments
./cmd/go/internal/gover/mod_test.go:		path, vers, _ := strings.Cut(f, " ")
./cmd/go/internal/modget/query.go:	pattern, rawVers, found := strings.Cut(raw, "@")
./cmd/go/internal/cmdflag/flag.go:	name, value, hasValue := strings.Cut(arg[1:], "=")
./cmd/go/internal/workcmd/edit.go:	before, after, found := strings.Cut(arg, "@")
./cmd/go/internal/workcmd/edit.go:	before, after, found := strings.Cut(arg, "=")
./cmd/go/internal/toolchain/select.go:		min, suffix, plus := strings.Cut(gotoolchain, "+") // go1.2.3+auto
./cmd/go/internal/toolchain/select.go:		name, val, hasEq := strings.Cut(a, "=")
./cmd/go/internal/toolchain/select.go:				name, _, _ := strings.Cut(a, "=")
./cmd/go/internal/toolchain/select.go:	path, version, _ := strings.Cut(pkgArg, "@")
./cmd/go/main.go:		_, dir, _ = strings.Cut(a, "=")
./cmd/distpack/pack.go:	version, rest, _ := strings.Cut(string(data), "\n")
./cmd/internal/testdir/testdir_test.go:		goos, goarch, ok := strings.Cut(*target, "/")
./cmd/internal/testdir/testdir_test.go:		line, actionSrc, _ = strings.Cut(actionSrc, "\n")
./cmd/internal/testdir/testdir_test.go:	header, _, ok := strings.Cut(src, "\npackage")
./cmd/internal/testdir/testdir_test.go:			if _, suffix, ok := strings.Cut(text, " "); ok {
./cmd/internal/testdir/testdir_test.go:		lines[i], _, _ = strings.Cut(lines[i], " // ERROR ")
./cmd/internal/testdir/testdir_test.go:		errFile, rest, ok := strings.Cut(errStr, ":")
./cmd/internal/testdir/testdir_test.go:		lineStr, msg, ok := strings.Cut(rest, ":")
./cmd/compile/internal/base/flag.go:		verb, args, found := strings.Cut(line, " ")
./cmd/compile/internal/base/flag.go:		before, after, hasEq := strings.Cut(args, "=")
./cmd/link/internal/ld/ld.go:		verb, args, found := strings.Cut(line, " ")
./cmd/link/internal/ld/ld.go:		before, after, exist := strings.Cut(args, "=")
./cmd/link/internal/ld/go.go:		line, data, _ = strings.Cut(data, "\n")
./cmd/link/internal/ld/go.go:			if before, after, found := strings.Cut(remote, "#"); found {
./cmd/cgo/internal/testcshared/cshared_test.go:				switch prefix, _, _ := strings.Cut(name, "-"); prefix {
./cmd/fix/typecheck.go:				if _, elem, ok := strings.Cut(t, "]"); ok {
./cmd/fix/typecheck.go:				if _, et, ok := strings.Cut(t, "]"); ok {
./cmd/fix/typecheck.go:				if kt, vt, ok := strings.Cut(t[len("map["):], "]"); ok {
./cmd/fix/typecheck.go:				_, value, _ = strings.Cut(t, "]")
./cmd/fix/typecheck.go:				if k, v, ok := strings.Cut(t[len("map["):], "]"); ok {
./cmd/api/main_test.go:			feature, approval, ok := strings.Cut(line, "#")
./cmd/doc/dirs.go:		path, dir, _ := strings.Cut(line, "\t")
./cmd/vendor/golang.org/x/tools/cmd/bisect/main.go:		if _, v, _ := strings.Cut(e, "="); strings.Contains(v, "PATTERN") {
./cmd/vendor/golang.org/x/tools/cmd/bisect/main.go:		k, v, _ := strings.Cut(x, "=")
./cmd/vendor/golang.org/x/tools/cmd/bisect/main.go:		line, all, _ = strings.Cut(all, "\n")
./cmd/vendor/golang.org/x/tools/go/analysis/passes/directive/directive.go:		line, text, _ = strings.Cut(text, "\n")
./cmd/vendor/golang.org/x/tools/go/analysis/passes/directive/directive.go:				_, line, ok = strings.Cut(line, "*/")
./cmd/vendor/golang.org/x/tools/internal/versions/versions.go:	v, _, _ = strings.Cut(v, "-") // strip -bigcorp suffix.
./cmd/vendor/golang.org/x/tools/internal/stdlib/stdlib.go:	typename, name, _ = strings.Cut(sym.Name, ".")
./cmd/vendor/golang.org/x/tools/internal/stdlib/stdlib.go:	recv, name, _ = strings.Cut(sym.Name, ".")
./cmd/vendor/golang.org/x/telemetry/internal/crashmonitor/monitor.go:		_, pcstr, ok := strings.Cut(line, " pc=") // e.g. pc=0x%x
./cmd/vendor/golang.org/x/telemetry/internal/config/config.go:			prefix, _, found := strings.Cut(c.Name, ":")
./cmd/vendor/golang.org/x/telemetry/internal/config/config.go:	prefix, rest, hasBuckets := strings.Cut(counter, "{")
./cmd/vendor/golang.org/x/telemetry/internal/counter/parse.go:		k, v, ok := strings.Cut(line, ": ")
./cmd/vendor/golang.org/x/telemetry/internal/upload/reports.go:				before, _, _ := strings.Cut(k, "\n")
./cmd/vendor/golang.org/x/build/relnote/relnote.go:	dir, rest, _ := strings.Cut(filename, "/")
./cmd/vendor/golang.org/x/build/relnote/relnote.go:	dir, rest, _ = strings.Cut(rest, "/")
./crypto/ecdsa/ecdsa_test.go:			curve, hash, _ := strings.Cut(line, ",")
./crypto/x509/pem_decrypt.go:	mode, hexIV, ok := strings.Cut(dek, ",")
./crypto/x509/verify_test.go:	before, _, ok := strings.Cut(string(out), ".")
./crypto/tls/handshake_test.go:		_, after, ok := strings.Cut(line, " ")
./crypto/tls/handshake_test.go:		before, _, ok := strings.Cut(line, "|")
./strconv/fp_test.go:	if mant, exp, ok := strings.Cut(s, "p"); ok {
./strconv/fp_test.go:	if mant, exp, ok := strings.Cut(s, "p"); ok {
./strings/example_test.go:		before, after, found := strings.Cut(s, sep)
./net/mail/message.go:		k, v, ok := strings.Cut(kv, ":")
./net/platform_test.go:	net, _, _ := strings.Cut(network, ":")
./net/platform_test.go:	switch net, _, _ := strings.Cut(network, ":"); net {
./net/platform_test.go:	switch net, _, _ := strings.Cut(network, ":"); net {
./net/url/url.go:	u, frag, _ := strings.Cut(rawURL, "#")
./net/url/url.go:		rest, url.RawQuery, _ = strings.Cut(rest, "?")
./net/url/url.go:		if segment, _, _ := strings.Cut(rest, "/"); strings.Contains(segment, ":") {
./net/url/url.go:		username, password, _ := strings.Cut(userinfo, ":")
./net/url/url.go:			if segment, _, _ := strings.Cut(path, "/"); strings.Contains(segment, ":") {
./net/url/url.go:		key, query, _ = strings.Cut(query, "&")
./net/url/url.go:		key, value, _ := strings.Cut(key, "=")
./net/url/url.go:		elem, remaining, found = strings.Cut(remaining, "/")
./net/main_posix_test.go:	net, _, _ := strings.Cut(network, ":")
./net/http/transport.go:			_, text, ok := strings.Cut(resp.Status, " ")
./net/http/response.go:	proto, status, ok := strings.Cut(line, " ")
./net/http/response.go:	statusCode, _, _ := strings.Cut(resp.Status, " ")
./net/http/request.go:	username, password, ok = strings.Cut(cs, ":")
./net/http/request.go:	method, rest, ok1 := strings.Cut(line, " ")
./net/http/request.go:	requestURI, proto, ok2 := strings.Cut(rest, " ")
./net/http/cgi/host_test.go:		k, v, ok := strings.Cut(trimmedLine, "=")
./net/http/cgi/host.go:		header, val, ok := strings.Cut(string(line), ":")
./net/http/cgi/child.go:		if k, v, ok := strings.Cut(kv, "="); ok {
./net/http/fs.go:		start, end, ok := strings.Cut(ra, "-")
./net/http/cookie.go:		name, value, found := strings.Cut(s, "=")
./net/http/cookie.go:	name, value, ok := strings.Cut(parts[0], "=")
./net/http/cookie.go:		attr, val, _ := strings.Cut(parts[i], "=")
./net/http/cookie.go:			part, line, _ = strings.Cut(line, ";")
./net/http/cookie.go:			name, val, _ := strings.Cut(part, "=")
./net/http/client_test.go:				first, rest, _ := strings.Cut(final, ",")
./net/http/main_test.go:		_, stack, _ := strings.Cut(g, "\n")
./net/smtp/smtp.go:			k, v, _ := strings.Cut(line, " ")
./net/main_test.go:		_, stack, _ := strings.Cut(s, "\n")
./go/types/eval_test.go:	before, after, _ := strings.Cut(s, sep)
./go/importer/importer_test.go:	compiler, target, _ := strings.Cut(export, ":")
./go/printer/comment.go:		line, text, _ = strings.Cut(text, "\n")
./go/printer/printer.go:	if p, _, ok := strings.Cut(prefix, "*"); ok {
./go/printer/printer.go:	before, _, _ := strings.Cut(last, closing) // closing always present
./go/constant/value_test.go:			if ns, ds, ok := strings.Cut(a[1], "/"); ok && kind == token.FLOAT {
./go/constant/value_test.go:	if as, bs, ok := strings.Cut(lit, "/"); ok {
./go/version/version.go:	v, _, _ = strings.Cut(v, "-") // strip -bigcorp suffix.
./go/doc/example_test.go:				name, kind, found := strings.Cut(sectionName, ".")
./go/doc/comment/text.go:			line, text, _ = strings.Cut(text, "\n")
./go/doc/comment/markdown.go:			line, md, _ = strings.Cut(md, "\n")
./go/doc/comment/print.go:			line, md, _ = strings.Cut(md, "\n")
./go/doc/comment/print.go:		line, rest, ok := strings.Cut(s, "\n")
./go/doc/comment/parse.go:		if _, b, ok = strings.Cut(b, "'"); !ok {
./go/doc/comment/parse.go:		if _, b, ok = strings.Cut(b, "."); !ok {
./go/doc/headscan.go:		inner, s, _ = strings.Cut(s[loc[1]:], html_endh)
./go/build/read_test.go:		beforeP, afterP, _ := strings.Cut(tt.in, "ℙ")
./go/build/read_test.go:		if beforeD, afterD, ok := strings.Cut(beforeP, "𝔻"); ok {
./go/build/build.go:		line, argstr, ok := strings.Cut(strings.TrimSpace(line[4:]), ":")
./go/build/build.go:	name, _, _ = strings.Cut(name, ".")
./go/build/vendor_test.go:			_, pkg, found = strings.Cut(fullPkg, "/vendor/")
./go/build/build_test.go:	errStr, _, _ = strings.Cut(errStr, ";")
./go/build/read.go:			path, _, ok = strings.Cut(args[1:], "`")
./go/build/constraint/vers.go:		_, v, _ := strings.Cut(z.Tag, "go1.")
./regexp/exec_test.go:				loStr, hiStr, _ := strings.Cut(pair, "-")
./regexp/exec_test.go:			if _, flag, ok = strings.Cut(flag[1:], ":"); !ok {
./regexp/regexp.go:		before, after, ok := strings.Cut(template, "$")
./regexp/syntax/parse.go:					lit, t, _ = strings.Cut(t[2:], `\E`)
./archive/tar/writer_test.go:		prefix, _, _ = strings.Cut(prefix, "\x00") // Truncate at the NUL terminator
./archive/tar/strconv.go:	ss, sn, _ := strings.Cut(s, ".")
./archive/tar/strconv.go:	nStr, rest, ok := strings.Cut(s, " ")
./archive/tar/strconv.go:	k, v, ok = strings.Cut(rec, "=")
./path/filepath/path.go:		part, p, _ = strings.Cut(p, "/")
./runtime/testdata/testprog/traceback_ancestors.go:				g, all, _ = strings.Cut(all, "\n\n")
./runtime/testdata/testprog/traceback_ancestors.go:					id, _, _ := strings.Cut(strings.TrimPrefix(g, "goroutine "), " ")
./runtime/pprof/proto_test.go:			in, out, ok := strings.Cut(tt, "->\n")
./runtime/pprof/pprof_test.go:		if _, s, ok = strings.Cut(s, t); !ok {
./runtime/pprof/pprof_test.go:	base, kv, ok := strings.Cut(spec, ";")
./runtime/pprof/pprof_test.go:	k, v, ok := strings.Cut(kv, "=")
./runtime/pprof/proto.go:		loStr, hiStr, ok := strings.Cut(string(addr), "-")
./runtime/trace_cgo_test.go:		prefix, tracePath, found := strings.Cut(got, ":")
./runtime/traceback_test.go:		_, tb, _ = strings.Cut(tb, "\n")
./runtime/traceback_test.go:		line, tb, _ = strings.Cut(tb, "\n")
./runtime/traceback_test.go:			funcName, args, found := strings.Cut(line, "(")
./runtime/traceback_system_test.go:		_, pcstr, ok := strings.Cut(line, " pc=") // e.g. pc=0x%x
./runtime/debug/mod.go:		line, data, ok = strings.Cut(data, newline)
./runtime/debug/mod.go:				key, rawValue, ok = strings.Cut(kv, "=")
./internal/testenv/testenv.go:				line, goMod, _ = strings.Cut(goMod, "\n")
./internal/testenv/testenv.go:			importPath, export, ok := strings.Cut(line, "=")
./internal/zstd/zstd_test.go:			want, _, _ := strings.Cut(name, ".")
./encoding/asn1/common.go:		part, str, _ = strings.Cut(str, ",")
./encoding/xml/typeinfo.go:	if ns, t, ok := strings.Cut(tag, " "); ok {
./encoding/xml/xml.go:	} else if space, local, ok := strings.Cut(s, ":"); !ok || space == "" || local == "" {
./encoding/json/tags.go:	tag, opt, _ := strings.Cut(tag, ",")
./encoding/json/tags.go:		name, s, _ = strings.Cut(s, ",")
./html/template/js.go:	mimeType, _, _ = strings.Cut(mimeType, ";")
./html/template/url.go:	if protocol, _, ok := strings.Cut(s, ":"); ok && !strings.Contains(protocol, "/") {
./html/template/attr.go:	} else if prefix, short, ok := strings.Cut(name, ":"); ok {
./testing/testing_test.go:				if name, _, ok := strings.Cut(trimmed, " "); ok {
./log/slog/slogtest_test.go:		kv, rest, _ := strings.Cut(s, " ") // assumes exactly one space between attrs
./log/slog/slogtest_test.go:		k, value, found := strings.Cut(kv, "=")
./mime/mediatype.go:	if major, sub, ok := strings.Cut(t, "/"); !ok {
./mime/mediatype.go:	base, _, _ := strings.Cut(v, ";")
./mime/mediatype.go:		if baseName, _, ok := strings.Cut(key, "*"); ok {
./mime/encodedword.go:	charset, text, _ := strings.Cut(word, "?")
./mime/encodedword.go:	encoding, text, _ := strings.Cut(text, "?")
./os/user/lookup_unix.go:		u.Name, _, _ = strings.Cut(u.Name, ",")
./os/user/cgo_lookup_unix.go:	u.Name, _, _ = strings.Cut(u.Name, ",")
./os/os_test.go:		host, _, ok := strings.Cut(hostname, ".")
./os/exec/exec_windows_test.go:		k, _, ok := strings.Cut(kv, "=")
./os/exec/exec_test.go:	errLine, body, ok := strings.Cut(string(bs), "\n")
./os/exec/exec.go:		k, _, ok := strings.Cut(kv, "=")
./text/template/option.go:	if key, value, ok := strings.Cut(opt, "="); ok {
```

</p>
</details> 




### Proposal

We propose adding the following functions:

#### For the `strings` package:

```go
// CutByte slices s around the first instance of sep,
// returning the text before and after sep.
// The found result reports whether sep appears in s.
// If sep does not appear in s, CutByte returns s, "", false.
func CutByte(s string, sep byte) (before, after string, found bool) {
    if i := IndexByte(s, sep); i >= 0 {
        return s[:i], s[i+1:], true
    }
    return s, "", false
}
```

#### For the `bytes` package:

```go
// CutByte slices s around the first instance of sep,
// returning the text before and after sep.
// The found result reports whether sep appears in s.
// If sep does not appear in s, CutByte returns s, nil, false.
// CutByte returns slices of the original slice s, not copies.
func CutByte(s []byte, sep byte) (before, after []byte, found bool) {
    if i := IndexByte(s, sep); i >= 0 {
        return s[:i], s[i+1:], true
    }
    return s, nil, false
}
```

### Rationale

The `CutByte` function is specifically optimized for cases where the separator is a single byte. In the analysis of Go's main repository and several large-scale Go projects, a significant number of string manipulations involve single-byte separators. The performance benefit of `CutByte` over `Cut` for these cases is approximately 25%, as measured in benchmarks comparing the two functions under similar conditions.

### Compatibility

`CutByte` is a new addition and does not modify any existing interfaces or behavior in the Go standard library. It follows the established patterns and naming conventions of the Go ecosystem, ensuring that it integrates seamlessly with the existing library functions.

### Implementation

The implementation of `CutByte` is straightforward and leverages the existing `IndexByte` function in both the `strings` and `bytes` packages. The proposed functions do not introduce any new dependencies or significant complexities.

### Conclusion

Adding `CutByte` to the Go standard library will provide developers with a more efficient tool for handling common string and byte slice operations involving single-byte separators. This function not only enhances performance but also maintains readability and simplicity, aligning with Go's philosophy of clear and efficient coding practices.