6 ms·
Concurrency issues aside, I've been working on a greenfield iOS project recently and I've really been enjoying much of Swift's syntax. I’ve also been experimen
by scottmf 9mo ago
Concurrency issues aside, I've been working on a greenfield iOS project recently and I've really been enjoying much of Swift's syntax.
I’ve also been experimenting with Go on a separate project and keep running into the opposite feeling — a lot of relatively common code (fetching/decoding) seems to look so visually messy.
E.g., I find this Swift example from the article to be very clean:
func fetchUser(id: Int) async throws -> User {
let url = URL(string: "https://api.example.com/users/\(id)")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(User.self, from: data)
}
And in Go (roughly similar semantics)
func fetchUser(ctx context.Context, client *http.Client, id int) (User, error) {
req, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
fmt.Sprintf("https://api.example.com/users/%d", id),
nil,
)
if err != nil {
return User{}, err
}
resp, err := client.Do(req)
if err != nil {
return User{}, err
}
defer resp.Body.Close()
var u User
if err := json.NewDecoder(resp.Body).Decode(&u); err != nil {
return User{}, err
}
return u, nil
}
I understand why it's more verbose (a lot of things are more explicit by design), but it's still hard not to prefer the cleaner Swift example. The success path is just three straightforward lines in Swift. While the verbosity of Go effectively buries the key steps in the surrounding boilerplate.
This isn't to pick on Go or say Swift is a better language in practice — and certainly not in the same domains — but I do wish there were a strongly typed, compiled language with the maturity/performance of e.g. Go/Rust and a syntax a bit closer to Swift (or at least closer to how Swift feels in simple demos, or the honeymoon phase)
- tidwall 9mo agoOr this. func fetchUser(id int) (user User, err error) { resp, err := http.Get(fmt.Sprintf("https://api.example.com/users/%d", id)) if err != nil { return user, err } defer resp.Body.Close() return user, json.NewDecoder(resp.Body).Decode(&user) }
- jtbaker 9mo agoI'm conflicted about the implicit named returns using this pattern in go. It's definitely tidier but I feel like the control flow is harder to follow: "I never defined `user` how can I return it?". Also those variables are returned even if you don't explicitly return them, which feels a little unintuitive.
- ragnese 9mo agoI haven't written any Go in many years (way before generics), but I'm shocked that something so implicit and magical is now valid Go syntax. I didn't look up this syntax or its rules, so I'm just reading the code totally naively. Am I to understand that the `user` variable in the final return statement is not really being treated as a value, but as a reference? Because the second part of the return (json.NewDecoder(resp.Body).Decode(&user)) sure looks like it's going to change the value of `user`. My brain wants to think it's "too late" to set `user` to anything by then, because the value was already read out (because I'm assuming the tuple is being constructed by evaluating its arguments left-to-right, like I thought Go's spec enforced for function arg evaluation). I would think that the returned value would be: `(nil, return-value-of-Decode-call)`. I'm obviously wrong, of course, but whereas I always found Go code to at least be fairly simple--albeit tedious--to read, I find this to be very unintuitive and fairly "magical" for Go's typical design sensibilities. No real point, here. Just felt so surprised that I couldn't resist saying so...
- jtbaker 9mo agoyeah, not really an expert but my understanding is that naming the return struct automatically allocates the object and places it into the scope. I think that for the user example it works because the NewDecoder is operating on the same memory allocation in the struct. I like the idea of having named returns, since it's common to return many items as a tuple in go functions, and think it's clearer to have those named than leaving it to the user, especially if it's returning many of the same primitive type like ints/floats: ``` type IItem interface { Inventory(id int) (price float64, quantity int, err error) } ``` compared to ``` type IItem interface { Inventory(id int) (float64, int, error) } ``` but feel like the memory allocation and control flow implications make it hard to reason about at a glance for non-trivial functions.
- hocuspocus 9mo agoNot defending Go's braindead error handling, but you'll note that Swift is doubly coloring the function here (async throws).
- tarentel 9mo agoWhat is the problem with that though? I honestly wish they moved the async key word to the front `async func ...` but given the relative newness of all of this I've yet to see anyone get confused by this. The compiler also ensures everything is used correctly anyway.
- hocuspocus 9mo agoThe problem is that that the Swift function signature is telling you that someone else needs dealing with async suspension and exception handling, clearly not the same semantics.
- vips7L 9mo agoIsn’t the Go version also forcing the callers to deal with the errors by returning them? The swift version just lets the compiler do it rather than manually returning them.
- tarentel 9mo agoIn a sense it is telling someone else that yes, but more importantly, it is telling the compiler. I am not sure what the alternative is here, is this not common in other languages? I know Java does this at least. In Python it is hidden and you have to know to catch the exception. I'm not sure how that is better as it can be easily forgotten or ignored. There may be another alternative I'm not aware of?
- saghm 9mo agoAnd sometimes you even have to use the return value the way the function annoates as well instead of just pretending it's a string or whatever when it's not! Having the language tell me how to use things is so frustrating.
- neonsunset 9mo agoC# :) async Task<User> FetchUser(int id, HttpClient http, CancellationToken token) { var addr = $"https://api.example.com/users/{id}"; var user = await http.GetFromJsonAsync<User>(addr, token); return user ?? throw new Exception("User not found"); }
- tarentel 9mo agoAs someone who has been coding production Swift since 1.0 the Go example is a lot more what Swift in practice will look like. I suppose there are advantages to being able to only show the important parts. The first line won't crash but in practice it is fairly rare where you'd implicitly unwrap something like that. URLs might be the only case where it is somewhat safe. But a more fair example would be something like func fetchUser(id: Int) async throws -> User { guard let url = URL(string: "https://api.example.com/users/\(id)") else { throw MyError.invalidURL } // you'll pretty much never see data(url: ...) in real life let request = URLRequest(url: url) // configure request let (data, response) = try await URLSession.shared.data(for: request) guard let httpResponse = response as? HTTPURLResponse, 200..<300 ~= httpResponse.statusCode else { throw MyError.invalidResponseCode } // possibly other things you'd want to check return try JSONDecoder().decode(User.self, from: data) } I don't code in Go so I don't know how production ready that code is. What I posted has a lot of issues with it as well but it is much closer to what would need to be done as a start. The Swift example is hiding a lot of the error checking that Go forces you to do to some extent.
- Jtsummers 9mo agoI'm not familiar with Swift's libraries, but what's the point of making this two lines instead of one: let request = URLRequest(url: url) let (data, response) = try await URLSession.shared.data(for: request) // vs let (data, response) = try await URLSession.shared.data(from: url) That aside, your Swift version is still about half the size of the Go version with similar levels of error handling.
- tarentel 9mo agoThe first one you can configure and it is the default way you'd see this done in real life. You can add headers, change the request type, etc. Likely, if you were making an actual app the request configuration would be much longer than 1 line I used. I was mostly trying to show that the Swift example was hiding a lot of things. The second one is for downloading directly from a URL and I've never seen it used outside of examples in blog posts on the internet.