Extended flavor of the Go programming language, aiming for increased value safety and maintainability.
The motivation behind the Goat project is presented in a series of blog posts presenting the upsides and downsides of Golang and setting the scene for Goat.
Goat specification is based on the premise of keeping things simple. We must comply with a very basic idea of Go: There shouldn't be more than one way to do something. Our basic rule is to not create fragmentations in Goat syntax.
Goat implementation will most likely be delivered as a code generation tool or as a transpiler producing regular go
files. However, full implementation details should be designed once the specification provided in this document
is finalized.
Goat syntax and rules are similar to those in Go, with the exception of the proposals presented below.
Motivation
The main vulnerability of the current visibility system of Go is naming collisions. Whenever you store a private type into a private variable or a public type into a public variable you may run into the following:
type user struct {
name string
}
func main() {
user := &user{name: "John"}
anotherUser := &user{name: "Jane"} // compilation error: user is not a type
}
In addition, the current visibility system of Go supports only two visibility modifiers - public and private. This prevents a more fine-grained control over visibility and produces another namespace pollution problem - all symbols in a package are automatically visible from all other files. This creates highly cluttered package namespaces, especially in big packages and big projects.
More context in this blog post section.
Solution
Goat should support 2 visibility modifiers:
package
- visible only within the current packagepublic
- visible everywhereEach symbol declaration must contain a visibility modifier, otherwise it will default to private. Name casing will not affect visibility. To prevent naming collisions, type names should be declared in upper case, and variables or functions should be declared in lower case.
Example
type User struct {} // visible only within the current file
package func doSomething() {} // visible only within the current package
public var answer = 42 // visible everywhere
var anotherAnswer = 43 // visible only within the current file
Motivation
Built-in functions contribute to namespace pollution and require additional cognitive load when making sure to avoid shadowing. Consider the following:
func larger(a, b []string) []string {
len := len(a)
if len > len(b) { // compilation error: invalid operation: cannot call non-function len (variable of type int)
return a
}
return b
}
To prevent shadowing of built-in functions we can simply convert them to keywords. However, a better solution would be to place them under a contextually oriented namespace when possible. This will both prevent the ability to override them and free those precious words to be used as safe variable names.
More context in this blog post section.
Solution
The following should be replaced:
append
- will be available as a slice method instead (i.e. slice.append(elems ...T)
).copy
- will be available as a slice method instead (i.e. slice.copy(dst []T)
).delete
- will be available as a map method instead (i.e. m.delete(key string)
).len
- will be available as methods of array
, slice
, string
and channel
(e.g. slice.len()
).cap
- will be available as methods of array
, slice
, string
and channel
(e.g. slice.cap()
).close
- will be available as a channel method instead (i.e. channel.close()
).make
- will be available using as a standard library function instead (goat.make
).complex
- will be available using as a standard library function instead (goat.complex
).real
- will be available using as a standard library function instead (goat.real
).imag
- will be available using as a standard library function instead (goat.imag
).print
- will be available using as a standard library function instead (goat.print
).println
- will be available using as a standard library function instead (goat.println
).panic
- will be a keyword to prevent shadowing.recover
- will be a keyword to prevent shadowing.new
- will be a keyword to prevent shadowing.error
- will be a keyword to prevent shadowing.Example
import "goat"
private func main() {
var slice = goat.make([]string)
slice = slice.append("a")
goat.println(slice.len())
goat.println(slice.cap())
var slice2 = goat.make([]string, 1)
slice.copy(slice2)
var m = map[string]string{"key": "value"}
m.delete("key")
var ch = goat.make(chan bool)
ch.close()
panic("panic is a keyword")
}
discussed in this issue.
More context in this blog post section.
Motivation
Lacking direct support for enums in Go has several drawbacks:
type ConnectionStatus int
const (
Idle ConnectionStatus = iota
Connecting
Ready
)
func main() {
var status ConnectionStatus = 46 // no compilation error
}
More context in this blog post section.
Solution
Introduce an enum
type to resolve the problems presented above.
Example
public type ConnectionStatus enum {
Idle
Connecting
Ready
}
private func example() {
var status ConnectionStatus // zero value is ConnectionStatus.Idle
fmt.Println(status == ConnectionStatus.Idle) // true
status = ConnectionStatus.Connecting
status.String() // "Connecting"
ConnectionStatus.allValues() // []ConnectionStatus{ConnectionStatus.Idle, ConnectionStatus.Connecting, ConnectionStatus.Ready}
ConnectionStatus.fromString("Idle") // ConnectionStatus.Idle
status = 0 // compilation error: invalid enum value
switch status { // compilation error: non-exhaustive enum switch statement
case ConnectionStatus.Idle:
return
}
}
In some cases, structs may be more than simply a collection of variables, but rather, a concise entity with state and behavior. In such cases, it may be required for some fields to initially hold meaningful values and not simply their zero values. We might need to initialize int values to -1 rather than 0, or to 18, or to a calculated value derived from other values. Go does not provide any realistic approach to enforce initial state in structs.
More context in this blog post section.
Solution
Provide initial value capability to struct literals.
Example
discussed in this issue.
In most other use cases, assignment to a variable is a single-time operation. Const assignments allow for preventing accidental shadowing and accidental rewriting of variables, and also allow code authors to convey intent.
discussed in this issue.
More context in this blog post section.
Motivation
Following up on this article.
Solution
Error handling in Goat should support the ?
operator. It will not break Go's premise around error handling. Error handling will remain explicit and should not be avoided. Propagation of errors, if needed, may be shortened to ?
operator.
Example
func concat1() (string, error) {
data1, err := fetchURLData("example.com")
if err != nil {
return "", err
}
data2, err := fetchURLData("domain.com")
if err != nil {
return "", err
}
return data1 + data2, nil
}
func concat2() (string, error) {
data1 := fetchURLData("example.com")?
data2 := fetchURLData("domain.com")?
return data1 + data2, nil
}
func concat3() string {
data1 := fetchURLData("example.com")? // compilation error: function must return error
data2 := fetchURLData("domain.com")? // compilation error: function must return error
return data1 + data2, nil
}
func fetchURLData(url string) (string, error) {
// ...
}
Motivation
Found in this blog post section.
Solution
In Goat, the go
keyword should return a promise.
Example
func fetchResourcesFromURLs(urls []string) ([]string, error) {
all := make([]promises.Promise[string], len(urls))
for i, url := range urls {
all[i] = go fetchResource(url)
}
return promises.All(all)
}
func fetchResource(url string) (string, error) {
// some I/O operation...
}
Goat conventions are similar to those in Go, with the exception of the proposals presented below.
Go omits visibility modifier keywords (public, private, etc...) in favor of symbol naming. Symbols starting with an uppercase letter are automatically public and the rest are private. While this is great for simplicity, over time it's becoming clear that this method has a stronger downside than upside: In most other languages, type names, by convention, begin with an uppercase letter, and variable names begin with a lowercase one. This convention has a very powerful implication - it means that variables can never shadow types. Consider the following Go code:
type user struct {
name string
}
func main() {
user := &user{name: "John"}
anotherUser := &user{name: "Jane"} // compilation error: user is not a type
}
This is quite common in Go, whenever you store a private type into a private variable or a public type into a public variable - you run into this. To prevent is we should introduce visibility modifiers, but also make sure that by contentions, types start with uppercase letters and variables with lowercase ones.
Found in this blog post section.
Acronyms in Go are uppercase by convention. This convention breaks readability and automatic tools when 2 or more acronyms are connected. For example - representing an HTTPS URL of some resource using the variable name HTTPSURL
instead of HttpsUrl
.
Found in this blog post section.
Following on this article, receiver names of a single letter are less meaningful and harder to maintain. There should be no problem with naming receiver variables self
.
Non sentinel errors should never use errors.New
, fmt.Errorf
, or similar. Rather, they should always define a dedicated error type.
Found in this blog post section.
Discussion of ideas and proposals should be done via issues. Issues can relate to an existing proposal or a discussion of new proposals.
Modifications to the spec should be done by submitting pull requests modifying this document with the required change. Each proposal should include:
The Go gopher eyes were designed by Renee French.
Her design is licensed under the Creative Commons 3.0 Attributions license.