On Typing Strings in Go

UPDATE:

Since writing these over 3 years ago I have since learned a lot more about GoLang and its type system. As such I now realize that GoLang has type aliases to address the need that I was writing an experience report about.

ORIGINAL POST:

One of the niftier aspects of programming in Go is you can create named types derived out of existing types.

For example, let’s consider storing and passing a URL. Depending on their needs programmers get to choose either a string or a class to represent URLs in many others languages. Often times programmers will choose a string to minimize the complexity of working with a URL as an object and for for the convenience of representing a URL as a literal, e.g. "https://google.com". But choice of a string results in loss of type safety for type-checked languages.

Types are types

Things are different with Go. In Go you can define a type that derives from other types, including basic types. And then we can use that type when declaring structure properties, parameters and variables and get full type safety.

For example, here we have a struct with a Website property declared as type URL. You can assign a string literal directly to a URL, but you cannot assign a variable of type string to a struct property declared as URL type. Further if you typecast the string to URL you of course can assign it to a URL type:

package main

type URL string

type Company struct {
    Name string
    Website URL
}
func NewCompany(name string) *Company {
    return &Company{
        Name: name,
    }
}
func (me *Company) SetURL(url URL) {
    // Type URL can be assigned directly to type URL
    me.Website = url
}
func main() {
    c := NewCompany("Google")
    // Use of a string literal okay 
    c.Website = "http://google.com"
    //...
    c.Website = "Facebook"
    url := "http://facebook.com"

    // Assign string to URL; this will NOT compile 
    c.Website = url

    // Assign string typecast to URL; this WILL compile	
    c.Website = URL(url)
}

Now you may feel this is nice but not compelling, and possibly not worth the effort of constant type-casting. And I would agree.

Differentiating similar/related types of values

But what inspired me to write this was the issues I was having keeping properties and parameters straight on a project related to differentiating between relatives URL paths and absolute URLs.

So consider this update. It adds a property that is designed to contain relative URL paths to products to our Company objects and a method named GetProductURLs() that returns an array of AbsoluteURLs. This does not really illustrate how having these types can help you keep the logic in your code straight in your head — that would require a much longer blog post — but it does illustrate the mechanics of typing strings and then casting them back and forth between string and declared types:

type AbsoluteURL string
type RelativeURLPath string

type Company struct {
    Name string
    Website AbsoluteURL
    ProductPaths []RelativeURLPath
}
func (me *Company) GetProductURLs() []AbsoluteURL {
    urls := make([]AbsoluteURL,len(me.ProductPaths))
    for i,pp := range me.ProductPaths {
        urls[i] = AbsoluteURL(fmt.Sprintf("%s/%s/",
            strings.TrimRight(string(me.Website),"/"),
            strings.TrimLeft(string(pp),"/"),
        ))
    }
    return urls
}
func main() {
    c := &Company{Name: "Microsoft"}
    c.Website = "https://www.microsoft.com"
    c.ProductPaths = []RelativeURLPath{
        "office",
        "azure",
        "windows",
        "sharepoint",
    }
    for _,u := range c.GetProductURLs() {
        fmt.Println(u)
    }
}

This is also an experience report.

All of the above is great. Really. But it also shows what a PITA Go can be regarding the need for casting when you start typing everything. Which makes me think, does it really need to be this difficult?

One (obvious-to-me) potential solution

And after contemplation I think it does not. Go already allows you to assign a string literal to any typed value receiver — such as a variable, parameter or structure property — so why not also allow assigning those same string-derived variables, parameters and properties anywhere a generic string value can be accepted?

Given a string-derived type is constraining a string, I cannot see how assigning a more constrained string to a less constrained string could cause any type safety concerns? So, assuming the Go team were to relax this constraint on more constrained types we could drop the string() casting and rewrite the operative part of the method GetProductURLs() making it just a bit easier to read:

urls[i] = AbsoluteURL(fmt.Sprintf("%s/%s/",
    strings.TrimRight(me.Website,"/"),
    strings.TrimLeft(pp,"/"),
))

Not hugely different, but in a large codebase this type of simplification can really add up in a good way.

And obviously this concept of more vs. less constrained does not apply only to string. It should apply to any types that are derived directly from other types in this form:

More Constrained vs. Less Constrained

Here is a simply example illustrating how the amount of constraint should allow typing to be relaxed at times:

type MoreConstrained LessContrained
type LessContrained string

var alpha MoreConstrained
alpha = "abc"

var alphanum LessConstrained
alphanum = "abc123"

alphanum = alpha // This should compile, rightly so
alpha = alphanum // This already fails to compile, rightly so

Call for consideration

Here is hoping that the architects of the Go language see this and agree enough with the concerns that they are willing to loosen the exact matching restrictions of types in Go.

Leave a Reply

Your email address will not be published. Required fields are marked *