Desde que tenho tido um caso de amor com a linguagem Go do Google, tenho passado muito tempo pensando profundamente, refletindo sobre os casos de uso de Go em termos de simultaneidade. Se você for como eu, vindo para Go depois de passar anos com sua mente sintonizada com “aquelas outras” línguas, você pode se encontrar às vezes lutando para adaptar sua mente à maneira de Go de fazer as coisas.
Portanto, vamos fazer uma pausa nos exemplos de codificação típicos que resolvem problemas do mundo real e vamos falar sobre como resolver um quebra-cabeça divertido com Go using channels. Este exercício é voltado para aqueles que são novos em Go e oferece uma estrutura para tocar e experimentar canais em Go.
Há um velho quebra-cabeça de travessia de rio chamado Fox, ganso e saco de feijão . De acordo com a Wikipedia, esse quebra-cabeça remonta ao século 9 e, desde então, entrou no folclore entre diferentes grupos étnicos. Acredito que encontrei o quebra-cabeça pela primeira vez em algum lugar na escola primária e a ideia é simples: um fazendeiro comprou um conjunto de três itens no mercado local. Ele comprou o seguinte: Uma raposa, um gopher e um saco de feijão. Em nossa versão desse quebra-cabeça, estaremos substituindo o ganso por ninguém menos que o mascote de Go – um esquilo. Acontece que há muitas variações no quebra-cabeça e você pode encontrar esse exercício de lógica em entrevistas de emprego .
O desafio é este: levar o fazendeiro e todos os três itens do mercado de um lado do rio para outro com as seguintes restrições. O fazendeiro deve pegar um barco com apenas um item para atravessar a margem do rio. Se ele deixar a raposa sozinha com o esquilo, a raposa comerá o esquilo. Se ele deixar o gopher sozinho com o saco de feijão, o gopher comerá o feijão. Portanto, o agricultor deve fazer com que todos os itens cruzem o rio de uma margem a outra em uma série de etapas que preservem seus itens de mercado.
Vamos mergulhar em uma maneira de modelar este quebra-cabeça em Go, mas considere a seguinte isenção de responsabilidade: No espírito de manter as coisas bastante simples, vamos usar apenas uma goroutine (a goroutine principal) e, além disso, estou não vai se preocupar em ir ao mar com abstrações e outros recursos da linguagem. Eu gostaria de manter este artigo focado. Além disso, este exercício visa modelar um quebra-cabeça e tentar resolvê-lo logicamente no código. Esta não é uma tentativa de fazer com que o computador resolva o quebra-cabeça usando algum algoritmo, mas tenho certeza de que você pode levá-lo lá, se quiser.
Para começar, vamos definir os tipos de que precisaremos:
//The good 'ol farmer
type Farmer struct {
}
//The gopher (goose) that the farmer purchased at the market
type Gopher struct {
}
func (g *Gopher) Eat(b *BagOfBeans, f *Farmer) {
if b != nil && f == nil {
log.Fatal("OOPS: Gopher just ate all the beans")
}
}
//The bag of beans that the farmer purchased at the market
type BagOfBeans struct {
}
//The fox that the farmer purchased at the market
type Fox struct {
}
func (fox *Fox) Eat(g *Gopher, f *Farmer) {
if g != nil && f == nil {
log.Fatal("OOPS: Fox ate gopher just now.")
}
}
//The boat which at any given time will only hold the farmer and ONE other item
type Boat struct {
farmer *Farmer
gopher *Gopher
fox *Fox
beans *BagOfBeans
}
//Defines a const of East or West
type Side int
const (
East = 0
West = 1
)
//The bank will represent the lands on either side of the river
type Bank struct {
farmer *Farmer
fox *Fox
gopher *Gopher
beans *BagOfBeans
side Side
}
//The river that the farmer must cross with all his belongings
var river = make(chan Boat, 1)
Como você pode ver, os tipos que representam cada um de nossos objetos são bastante diretos. A maioria deles está simplesmente vazia, onde não estamos interessados em manter nenhum estado. Algumas coisas a serem destacadas: O rio é um tipo de canal que pode conter um barco com um buffer de um. Esta é a oportunidade perfeita para perceber que um canal atua muito como um rio ou riacho, transportando dados por ele. Ele oferece um meio de comunicação que pode ser usado com segurança entre goroutines. Novamente, em nosso exemplo, precisamos usar apenas uma goroutine para cumprir nosso objetivo aqui hoje.
Além disso, observe que tanto o Gopher quanto o Fox têm funções de receptor Eat associadas a eles. Quando qualquer um desses métodos é executado, se o fazendeiro não estiver presente (nulo) e o item comestível apropriado ainda estiver presente, o aplicativo fará um log.Fatal () para sinalizar ao usuário que a raposa comeu o esquilo ou o esquilo comeu o feijão.
Mais uma coisa: eu segui o caminho de ter o tipo de barco contendo apenas o agricultor e um outro item, mas depois tornou o código mais complicado e achei que seria mais fácil demonstrar este quebra-cabeça se tratarmos o barco como um contêiner que só pode conter o agricultor e um slot para um outro item. Podemos colocar qualquer objeto em um slot disponível, mas por convenção o barco não pode conter mais de 2 itens com o agricultor incluído.
Continuando, vamos criar nossas instâncias em nossa função principal:
func main() {
//The west bank starts off empty
westBank := Bank{side: West}
//The east bank starts off with the farmer and his store bought items
eastBank := Bank{side: East}
//We create an instance of the farmer, fox, bag of beans, and lastly the gopher
eastBank.farmer = &Farmer{}
eastBank.fox = &Fox{}
eastBank.beans = &BagOfBeans{}
eastBank.gopher = &Gopher{}
//...code continues
}
Começamos com duas margens em cada lado do nosso rio. Em nossa versão do quebra-cabeça, o fazendeiro com sua gopher, raposa e saco de feijões começará na margem leste. Esta é basicamente nossa configuração do quebra-cabeça com o código acima representando nosso estado inicial. O que acontecerá é que, para cada etapa, carregaremos o barco com pelo menos o agricultor e possivelmente um outro item. Em nosso quebra-cabeça, vamos considerar que para qualquer objeto dado (fazendeiro, gopher, raposa ou feijão), apenas uma referência pode ser obtida por vez. Em outras palavras, se tivermos um ponteiro para o fazendeiro na margem oeste, e o fazendeiro entrar no barco e sair da margem oeste, a margem oeste não deve mais ter uma referência ao ponteiro, então a referência da margem oeste ao fazendeiro será nulo. Esse será o caso de todos os itens principais para o nosso quebra-cabeça funcionar.
Precisaremos de mais alguns métodos:
func (b *Bank) checkState() {
counter := 0
if b.fox != nil {
counter++
b.fox.Eat(b.gopher, b.farmer)
}
if b.gopher != nil {
counter++
b.gopher.Eat(b.beans, b.farmer)
}
if b.beans != nil {
counter++
}
if b.farmer != nil {
counter++
}
sd := "West"
if b.side == East {
sd = "East"
}
fmt.Printf("%s bank: %+v\n", sd, b)
//should the counter be 4, this means all 4 made it!
if counter == 4 && b.side == West{
fmt.Println("!!YOU WIN!!")
}
}
func (b *Bank) Send(boat Boat) {
b.farmer = nil
if boat.gopher != nil {
b.gopher = nil
}
if boat.beans != nil {
b.beans = nil
}
if boat.fox != nil {
b.fox = nil
}
b.checkState()
river <- boat
}
func (b *Bank) Receive() {
boat := <-river
b.farmer = boat.farmer
if boat.gopher != nil {
b.gopher = boat.gopher
}
if boat.beans != nil {
b.beans = boat.beans
}
if boat.fox != nil {
b.fox = boat.fox
}
b.checkState()
}
Em nosso quebra-cabeça, o que acontecerá é que teremos vários estágios em que o barco estará viajando de volta e quarto, entre a margem oeste / leste. Cada vez que o barco é enviado ou recebido, precisamos verificar o estado das coisas. Lembre-se, com nosso quebra-cabeça, que se deixarmos a raposa sozinha com a esquife, a raposa comerá a esquife. Se deixarmos o gopher sozinho com o feijão, isso significa que não há mais feijão para o nosso fazendeiro. Quando qualquer um desses eventos ocorrer, consideramos isso o fim do jogo e fazemos um log.Fatal ().
Além da função checkState (), também temos as funções Send e Receive que chamamos de um determinado banco. Essas funções cuidam de lidar com os detalhes de propriedade sobre quais ponteiros devem ser nulos e quais ponteiros devem ter uma referência adequada a um objeto sempre que a propriedade muda. Além disso, essas funções certificam-se de chamar checkState para validar o estado das coisas.
Executando o código
Ao executar o código, você descobrirá que ele emitirá o estado dos bancos a cada comando Enviar / Receber. Você verá basicamente como cada banco retém e depois perde suas referências aos objetos que estão viajando de barco. Aqui está um segmento de alguns exemplos de como isso se parece:
//Not Go code, just output from the program
//Send
East bank: &{farmer:<nil> fox:0x176a68 gopher:<nil> beans:0x176a68 side:0}
//Receive
West bank: &{farmer:0x176a68 fox:<nil> gopher:0x176a68 beans:<nil> side:1}
//Send
West bank: &{farmer:<nil> fox:<nil> gopher:0x176a68 beans:<nil> side:1}
//Recieve
East bank: &{farmer:0x176a68 fox:0x176a68 gopher:<nil> beans:0x176a68 side:0}
//Send
East bank: &{farmer:<nil> fox:<nil> gopher:<nil> beans:0x176a68 side:0}
//...
//...
//...
Interpretar o código acima é um pouco estranho no início. No primeiro Send, vemos que a margem leste perde sua referência para o fazendeiro e o gopher. Por quê? Porque eles estão a caminho do Ocidente. Em seguida, a Cisjordânia recebe o barco e, como você pode ver, agora tem uma referência ao fazendeiro e ao esquilo. Isso é repetido até que todo o fazendeiro e todos os itens tenham se mudado inteiramente da margem leste para a oeste.
Resolvendo o quebra-cabeça:
Com tudo agora configurado, resolver o quebra-cabeça é apenas uma questão de seguir uma série de etapas para resolvê-lo. Aqui estão algumas linhas de exemplo de código:
//Stage 1: send the farmer and the fox
eastBank.Send(Boat{farmer: eastBank.farmer, fox: eastBank.fox})
westBank.Receive()
//Stage 2: send the farmer back by himself
westBank.Send(Boat{farmer: westBank.farmer})
eastBank.Receive()
Já posso te dizer, o código acima irá falhar imediatamente. Se você executar o código acima, ele nem mesmo limpará o primeiro estágio porque você está enviando o fazendeiro e a raposa. Isso significa que o gopher irá mastigar alguns feijões e o jogo terminará.
Vou deixar a solução desse quebra-cabeça como um exercício para o leitor. Na verdade, existem algumas maneiras de ganhar, e se você resolver o quebra-cabeça com a sequência correta, o jogo detectará isso automaticamente no método checkState () e Println (“!! VOCÊ GANHOU !!”) na tela.
Além disso, aqui está um link para o código-fonte completo em toda a sua glória com a solução .