I've done a decent amount of work in both - I've written a compiler in OCaml and a few applications in Haskell.
I do find Go to have the "best of both worlds" when it comes to the type system (ignoring all other features of the languages).
As you know, one of the advantages of writing in dynamic languages like Ruby and Python is that you don't have to think much before writing down the first few lines of (working, runnable) code. This makes it really easy to sit down at a keyboard and bang out a prototype, or even a one-off utility script.
With Haskell, I have to spend a lot of time thinking up-front about how to design my types, because the arrangement of the datatypes largely determines the functionality of your program. The upside is that, once you've sketched out what you need your types to look like in Haskell, the entire structure of your application has essentially been stubbed out - you simply need to fill in the gaps. This is really nice when making incredibly polymorphic functions - as has been pointed out, you can implement an entire Haskell library without understanding anything about the logic, operating solely off of the type annotations[0].
The downside is that it takes longer to get those first few lines of (working, runnable) code down. Maybe that's not a big loss, or maybe it is. But Go does provide a lot more type safety than any of C or Java (and certainly a lot more than any dynamic language than Ruby or Python).
I'm a functional programmer at heart, and given my druthers, I'd probably prefer a little more type safety in Go, rather than less. But having written a lot of Go over the last year, I don't find myself commonly running into the sorts of errors that a stronger type system would have prevented.
In short, (IMHO), the biggest additional benefit to strengthening a type system like Go's to resemble Haskell's would be in how it forces you to change the code-formulating process, not in how it prevents more errors. Unfortunately, this is also the biggest drawback to strengthening the type system - it means one can no longer use the same programming workflow that one uses for Python, Ruby, Java, etc.
I personally tend to see it as the worst of both worlds. I have to deal with types _and_ I don't really get strong guarantees. I don't run into problems in Ruby that Go's type checker could help with. But that's also my personality: I like extremes.
I'm curious what guarantees you're looking for that you don't get with Go that you'd get in other languages. I haven't worked with the more esoteric languages like OCaml and Haskell, so I might be missing something simple.
First of all, I just want to say that I don't think that Haskell or Rust are competing with Go, we're just talking type systems here. I'll use those two because they're my favorite. Three examples, one from each and one they share:
In Haskell, the type system ensures that side effects only happen in one place: things inside a monad of some kind. For example, if I'm given a function:
foo :: Int -> Int
I _know_ for a fact that this function doesn't do any IO. It doesn't maintain any state. It won't launch the missiles. And I also know that everything needed to understand what goes on in `foo` will happen via the one parameter. Because it's annoying to pass around code that interacts with the outside world, you end up with a small shell of imperative, stateful code, and a large amount of stateless, pure functional code. Since Go (to my knowledge) doesn't enforce referential transparency, it won't do this.
Haskell and Rust both don't have the concept of null. This is fantastic, as even the inventor of null thinks it's a bad idea. It's borderline irresponsible to write a new programming language with nulls today. So how do they handle a computation that may fail? Higher order types:
use std::option::Option;
fn call_me_maybe(x: int) -> Option<int> {
if(x > 5) {
Some(x)
} else {
None
}
}
fn main() {
let i = 6;
match call_me_maybe(i) {
Some(x) => println!("Yes! {:i}", x),
None => println!("nope"),
}
}
This will print "Yes! 6". If you change it to 5, it will print "nope". The higher type wraps the output and tell us if we've succeeded or failed. Here's the kicker: what happens if we leave off the error case?
rust.rs:13:8: 15:9 error: non-exhaustive patterns: None not covered
rust.rs:13 match call_me_maybe(i) {
rust.rs:14 Some(x) => println!("Yes! {:i}", x),
rust.rs:15 }
It'll make us handle it. And since it has a different type, we can't pass it to something that takes an `int` either, because it's an `Option<int>`. Both Rust and Haskell can do this, and have tools to make this easier, too. For example, maybe you want an error message, so Rust's Result type has a message on the fail case. Or you need three or more states, you can build your own.
Finally, in Rust, data is immutable by default:
let i = 6;
i = i + 5;
rust.rs:3:8: 3:9 error: re-assignment of immutable variable `i`
rust.rs:3 i = i + 5;
^
rust.rs:2:12: 2:13 note: prior assignment occurs here
rust.rs:2 let i = 6;
And pointers have explicitly one owner or many owners:
fn main() {
let i: ~int = ~6; // read: i is an owned pointer to an int.
let j: @int = @6; // read: j is an managed pointer to an int.
let k = i;
let l = j;
println(i.to_str()); // error: use of moved value: `i`
println(j.to_str()); // this is fine
}
So in Rust, you cannot share data that has multiple owners across threads:
fn main() {
let i: ~int = ~6; // read: i is an owned pointer to an int.
let j: @int = @6; // read: j is an managed pointer to an int.
do spawn {
println(i.to_str()); // fine, prints
}
println(i.to_str()); // you can't use it after you've given it away, either!
// error: use of moved value: `i`
// println(i.to_str()); //
// ^
//note: `i` moved into closure environment here because it has type `~fn:Send()`, which is non-copyable (perhaps you meant to use clone()?)
//do spawn {
// println(i.to_str()); // fine, prints
//}
do spawn {
println(j.to_str()); // error: cannot capture variable of
// type `@int`, which does not fulfill
// `Send`, in a bounded closure
}
}
Therefore, race conditions are a compile-time error. Go has a race detector, which can help, but it can't always help.
Anyway, hopefully that explains some of my beef with Go's types. I think Go gets a lot of things right, I think the type system is one place where it gets things really wrong.
I'm just curious, how much time have you spent with a really strongly statically typed language like Haskell or OCaML?