I'm working on a webapp in Go and I'd like to have a unified interface for frontend, controller, and storage. To that end, I have a Post type which has ID, UserID, Title, and Text. However, when creating the post, there's no way for any layer except for the storage layer to know what the Post's ID should be. What is the idiomatic way to represent an optional value like this?
评论:
jerf:
SilverWingedSeraph:Based on your description, it is very likely you can use composition:
type PostContents struct { UserID UserID `json:"user_id"` Title string `json:"title"` Text string `json:"text"` } type Post struct { PostID int `json:"id"` PostContents }
The reason I say this is that it is likely that if you exercise type discipline consistently you will find that at any given point in the program, you will definitively have either a
PostContents
or aPost
. If you are careful with your types, you are likely to find there is never a point where you have "maybe a post with an ID, maybe a post without an id". Though it does sometimes take a bit of practice to get good enough with typed programming to be correct enough in your handling to get this correct.The next tier of flexibility is interfaces. If you do have a place where you may be manipulating "a post that may or may not have an ID", you can declare an interface:
type PostContentsI interface { GetUserID() UserID GetTitle() string GetText() string }
and implement it on
PostContents
. By the way composition works, that will automatically implement it on thePost
I show above, soPost
will automatically conform toPostContentsI
. This is a bit smellier than the above, but is still statically-typed and has compile-time guarantees that anything usingPostContentsI
will still not manipulate the ID that may or may not exist. This is especially true if the value being passed by interface doesn't really "go" anywhere else after that; for instance, one thing I would accept without too much thought in this context would be aRender(p PostContentsI, w io.Writer)
method since it is completely plausible to me that you may want to render a post prior to it being given an ID. (For instance, a live preview function in the post creation UI.)(I don't love the name PostContentsI, but without more context I don't know what you might want to call this. RenderablePost would be plausible if that's your primary use for the interface, for instance. Which you'll probably find evolving into a
Renderable
on you....)Only as a last resort would I consider using a pointer to the ID that may or may not be nil, in a
Post
struct that tries to encompass both cases. That puts all the onus on you to ensure that every time you go to use an ID, you must first check that it exists. Better to let the compiler do that with the types above.There isn't that much daylight between an expert Go programmer and a beginner in my opinion, at least not compared to most languages, but fluidity with the compositional style rather than the inheritance style is definitely one of the distinctions between beginner and expert.
itsmontoya:Thank you very much for this in-depth response! It's exactly what I was looking for.
throwawayguy123xd:If your payload is json, you can setup omitempty fields
egonelbre:you make an interface, and then your type can have functions
func (post p) getID() int { return p.id }
notsoobadusername:package post type ID int64 type Post struct { ID ID `json:"id,omitempty"` Owner user.ID `json:"owner"` Title string `json:"title"` Text string `json:"text"` } func (p *Post) IsStored() bool { return p.ID == 0 }
egonelbre:Quick question: Why not:
ID int64 `json:"id,omitempty"`
Usually you end up with a lot of different IDs, which can be easily mixed up. So just some typesafety at the cost of having to deal with explicit casts some times.
