Request for comments for a experienced web dev new to go?

blov · 2017-03-25 16:00:16 · 639 次点击    
这是一个分享于 2017-03-25 16:00:16 的资源,其中的信息可能已经有所发展或是发生改变。

Hi, I have about 8-9 years of exp in web dev, company is moving to use go from php. I decided to try to learn go by writing out the classic card game war.
After playing around with it a bit, I decided to see what the community would say and comment about.

The URL for the repo is: https://github.com/smergler/war_go

If this is the wrong subreddit, please let me know.

Thanks!


评论:

DenzelM:

Welcome Sloan! This is absolutely the right subreddit, as evidenced by all the helpful comments so far. You're already well on your way, so I'm going to try to provide you with some guidance on how to jump to the next level.

To start off, you did a great job separating responsibilities, putting a structure in place, and writing understandable tests. It's very easy to follow the flow of logic in your code and your tests.

I'm going to attack this in two ways. First, I'm simply going to treat the code as-is and comment on syntactical and semantic improvements; ways to improve the idiomatic character of the code. Then, I'm going to comment on places where I believe structural improvements can be made.

Before I do that, there are some minor code formatting issues. Luckily, formatting can be taken care of in an automated fashion with Go's gofmt tool.

Ok, let's talk about the syntax/semantics:

  • Slice expressions
  • Appending one slice to another slice
  • For-Range loops
  • Print() with String()
  • Implicit/explicit initialization of empty types
  • Pointer vs. value types for structures

Slice expressions

When re-slicing a slice in the case of expressions like s[1:len(s)], you can simplify to s[1:] which implies slicing from one to the end. Slice expressions is a great section, in the Go specification, on all the ways you can slice and dice.

Appending one slice to another slice

Go provides some simple syntax for appending one slice to another too; making your life a lot easier. (Pro tip: you can actually use this destructuring-like syntax for any variadic function.)

Transforming:

for i := 0; i < len(cardsInPlay); i++ {
    p_winner.Deck = append(p_winner.Deck, cardsInPlay[i])
}

Into:

p_winner.Deck = append(p_winner.Deck, cardsInPlay...)

For-Range loops

Go provides a useful range clause for iterating over the values of an array, slice, string, map, or even a channel. It saves the tedium of writing a loop and using an index to iterate over data.

Transforming:

for suit_num := 0; suit_num < len(suits); suit_num++ {
    // suit = suits[suit_num]
}

Into:

for _, suit := range suits {
    // ...
}

Print() with String()

The Stringer interface, which is implemented by any type that has a String() string method, is used to print values passed as an operand to any formatting method like Printf() or even unformatted methods like Print().

Since you implemented the Stringer interface for you Card type, that makes this redundant:

fmt.Printf("%s's card: %s\n", h.P1.Name, p1_card.String())

Instead:

fmt.Printf("%s's card: %s\n", h.P1.Name, p1_card)

Works just as well.

Implicit/explicit initialization of empty types

Whenever you declare or construct a new variable it will be initialized with its zero value, if you choose not to explicitly initialize it. Every basic type has a zero value, and the zero value of a composite type is made up of the zero values of its constituent parts.

For example, the zero value of any string is "". Therefore, when declaring var s string, the variable s is implicitly initialized to the zero value of "" without having to explicitly initialize it. We can use this property with structures, a composite type, as well. So that:

return Card{
        Rank: "",
        Suit: "",
    }

Becomes:

return Card{}

And the constituent parts, Rank and Suit, will be initialized to their respective zero values (the empty string in this case). You can use this fact to your advantage many times to simplify your code.

Pointer vs. value types for structures

Generally, when you're working with composite types, you want to pass them around as pointers instead of values. As long as these composite types are not opaque references, e.g., slices, maps, etc. It's far more efficient to pass around a pointer to a structure instead of copying the value of the structure for each procedure call or return.

When you pass a pointer, the runtime only has to copy over 4 bytes (for a 32-bit system) or 8 bytes (for a 64-bit system). When you pass the whole structure by value, it has to copy over N bytes, where N is however many bytes the entire structure takes up in memory.

Now, let's talk about some of the structural/architectural improvements that can be made. You're already 80-90% of the way there, and we're going to explore taking it even further:

  • Go type crazy
  • Representing Rank and Suit
  • Yank Deck from Player
  • Represent a Pile of Cards?
  • Type constructors

Go type crazy

Go's types are so lightweight. They make it easy to represent any concept you want. I find it really helpful to represent even the smallest concepts in the domain of what I'm working on with a type.

Go's types are very lightweight. It's easy to represent any concept you want. I find it really helpful in representing even the smallest domain concepts in projects I'm working on. If []Card represents a deck of cards, then why not call it as such with a type Deck []Card declaration. Easy enough. And that illuminates a problem doesn't it... because not just any amount of cards is a deck? In fact, a Deck represents a type concept with very specific type invariants. More on this idea later.

Let's talk about a better representation for the rank and suit of a card.

Representing Rank and Suit

Remember, our types should represent our domain concepts. So let's name them as such:

type Suit string
type Rank string

But we can make things even better. Let's represent Rank as an integer, and then convert that to a name later. You've got the right idea, by constructing a map from a cards name to its rank. We're going to need a way to translate between the name and value of ranks if we want to print it out later. However, let's invert the representation. If you think about it, that's what a rank really is, an integer value. We'll make it type Rank int.

Now, we're going to have to create the actual ranks. We can do that with Go's convenient little iota identifier to create successive, increasing integers. We're going to create a series of constants that work sort of like a typed enumeration.

const (
    Ace Rank = iota
    One
    Two
    // ...
    King
)

That way we can do things like:

r1 := Rank{Jack}
r2 := Rank{King}
r1 > r2

We can even implement the Stringer interface on Rank itself and use that when printing out the rank as a string. But how are we going to translate from an integer value to a string. Well, we can just flip your idea from before, create a package-private map to translate from Rank to string, and then use that for the string method. Something like:

var rankNames = map[Rank]string{
    Ace: "A",
    King: "K",
    // ...
    One: "1",
}
func (r Rank) String() string { return rankNames[r] }

I'll leave the update of Card and the IsBetterThan method to you. It should be a lot easier, more straightforward, and simpler now.

Yank Deck from Player

Let's continue using our type crazy powers. A Player has a deck, but a player isn't a deck itself. Therefore, ShuffleDeck(), IsDeckEmpty(), and DeckSize() should not be defined on a Player. We can extract a deck out into its own type to represent that domain-level concept.

type Deck []Card

Then we can define methods on that domain-level concept:

func (d *Deck) ShuffleDeck()
func (d *Deck) IsDeckEmpty() bool
func (d *Deck) DeckSize() int

And use that concept explicitly throughout the program. Which leads us to an interesting question though. There's one more concept that may be especially helpful to represent for a card game like War.

Represent a Pile of Cards?

Did you notice that when you're playing the game of War, that you're representing the middle pile of cards as []Card. That makes enough sense. But we could also move it to a first level concept! Making:

type Pile []Card

And then implementing the appropriate methods on a pile of cards. Methods to take away cards, add cards, or even add whole other pile. This makes sense. At the end of the day, the pile of cards in play is quite literally a pile. So then our deck becomes a specific type of pile:

type Deck Pile

Why would we do that? Well it depends on how detailed you want to be with your domain concepts. But generally, a Deck of cards has a specific invariant that can't be violated. Namely, there can never be >52 cards, and no cards can be repeated. This is an invariant that a Pile of cards does not have. It maps pretty nicely to our idea of how things work in the real-world.

Type constructors

Finally, the logic that you have in Game.SetUpDeck() to create a deck is actually the responsibility of a type constructor. A type constructor for Deck will construct it so as to not invalidate any invariants and maintain them throughout the lifecycle of the object.

Idiomatic type constructors in go start with "New" and usually describe the type to be constructed. In the case of your Deck it'd look something like:

func NewDeck() *Deck

Code Review: Complete

That's everything I have for my code review, Sloan. Hopefully it's been helpful. Feel free to post here again with an update, for feedback on anything else, or just to ask a question. We love to help around here!

piratelax40:

just wanted to say awesome review - I picked up some new tips and ideas as well

xiegeo:

For someone from php, the biggest thing to learn is the type system. It takes a while to get, but once you do, you won't want to go back to an untyped language for building any complex system.

miko5054:

IMO adding interfaces and packages will make the code more organize

golang_throwaway:

thanks

dinkumator:

I'd use some types for the suit and ranks. You can have string methods to pretty print them and just integer enums

golang_throwaway:

thanks, I'll check that out


入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

639 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传