Podemos pensar no Go como um C mais limpo: estruturas (structs) como no C, libertação automática de memória (há garbage collection), referências (sem aritmética de ponteiros) e valores, funções e os tipos de dados usuais. As funções podem retornar tuplos, mas os tuplos não são valores de primeira-classe, i.e. não podem ser alocados ou guardados numa variável.
Exemplo de uma função que retorna um tuplo:
func foo(int v) (int v1, int v2) {
return v, v*2
}
a, b = foo(3);
Temos a palavra chave func, o nome da função foo, os parâmetros da função (int v) e os parâmetros de saída (int v1, int v2). É o regresso dos parâmetros de saída no sentido em que podemos manipulá-los, mas sintacticamente não estão ao lado dos parâmetros de entrada, o que me parece uma boa escolha. O mesmo exemplo pode ser reescrito assim (mais estranho na minha opinião):
func foo(int v) (int v1, int v2) {
v1 = v;
v2 = v * 2;
return;
}
a, b = foo(3);
Existem dois conceitos que merecem realce: métodos e interfaces. Os métodos são funções que se associam a um tipo de dados e são utilizados como os métodos do C++ ou do Java. Não podem ser associados a interfaces; podem ser associados tipos, por exemplo a uma estrutura. A definição de um método é isolada, não é feita na definição do tipo ao qual está associada.
Exemplo ilustrativo de um método que calcula a norma sobre o tipo de dados ponto:
// Disclairmer: o exemplo foi feito por mim sem testar se funciona
struct Point {
int x;
int y;
}
func (Point self) Magnitude() int {
return Math.sqrt(self.x * self.x + self.y * self.y)
}
Point p = Point{0, 0};
p.Magnitude(); // returns 0
p.x = 2;
p.Magnitude(); // returns 2
Resumindo, para definir um método temos a keyword func, entre
parêntises o tipo ao qual o método vai estar associado e o nome da
variável (neste caso usei self), de seguida os parâmetros e o tipo de
retorno (ou parâmetros de saída).
Um interface identifica (estruturalmente) tipos que tenham um dado
conjunto de métodos associados. Ou seja, se dois valores tiverem pelo
menos os mesmos métodos que um dado interface, então são considerados
daquele interface. A associação a um dado interface é implicita, não
existe associação, e.g. T1 implements ISomeInterface.
Concorrência
Existe paralelismo de tarefas, nesta linguagem é feita através de uma
goroutine (semelhantes a threads ou tasks). As goroutines são (pretendem ser) leves. Para criar uma goroutine, colocamos a keyword go antes de uma chamada de uma função:
go LongComputation();
A comunicação entre tarefas parelelas é feita através de memória
partilhada ou através de canais. Por exemplo, para criar um canal que transmite inteiros e associá-lo a uma variável c, faz-se:
c := make(chan int)
Podemos usar esse canal no exemplo seguinte em que criamos duas tarefas paralelas (goroutines) uma envia mensagem pelo canal c e a outra tarefa recebe mensagens pelo mesmo canal, mostrando o valor recebido no ecrã.
func client() {
c<- 10;
}
func server() {
for { // this for is equivalente to C's for (;;)
msg := <-c;
fmt.Printf("received %d\n", msg);
}
}
O select da linguagem é muito poderoso e podem fazer-se servidores/clientes mais complicados com brevidade. O seguinte exemplo está na especificação da linguagem.
var c, c1, c2 chan int;
var i1, i2 int;
select {
case i1 = <-c1:
print("received ", i1, " from c1\n");
case c2 <- i2:
print("sent ", i2, " to c2\n");
default:
print("no communication\n");
}
for { // send random sequence of bits to c
select {
case c <- 0: // note: no statement, no fallthrough, no folding of cases
case c <- 1:
}
}
Num select com várias operações de recepção (<-c) e/ou de envio (c<-) de mensagem é tratada de cima para baixo o primeiro ramo que esteja activo. Caso não hajam expressões de envio/recepção de mensagem activas, é avaliado o caso default. Se o ramo default não estiver definido, então o select fica bloqueado até uma expressão de envio/recepção esteja activa. No exemplo acima, o primeiro select tenta receber uma mensagem pelo canal c1. Caso não haja mensagem para se receber no canal c1, o select tenta enviar uma mensagem pelo canal c2. Se não houver transmissão da mensagem, então é avaliado o ramo default e mostra-se no ecrã no communication. De seguida temos um select dentro de um ciclo, o Go alterna de uma forma uniforme e justa (não está provado) entre os vários ramos activos, enviando por este motivo ora o valor 1 ora o valor 0 pelo canal c.
Como os canais são valores, podem ser enviados através de um canal. Existe primitivas de comunicação síncrona e assíncrona. É possível tipificar canais, especificando o
tipo de valores comunicado e se é de entrada ou de saída. Não há
suporte para tipos de sessão.
Conclusão
O Go é vendida como uma linguagem de sistemas (concorrência incluida).
Se me dessem para escolher C ou Go ignorando o contexto de utilização,
escolheria Go sem piscar os olhos. Escolheria também Go ao invés do C++.
Acho que tem uma elegância sintática razoável (para uma linguagem que segue a sintaxe do C) e que faz um bom compromisso nas primitivas e abstracções escolhidas: gosto muito do sistema de interfaces mas sinto falta de excepções.
A resposta à concorrência é insuficiente. Um slogan do Go é, e passo a citar:
Do not communicate by sharing memory; instead, share memory by communicating.
Mas entre o que se apela e o que se faz vai um grande passo. Por mais que existam canais e de existir o slogan, continua a ser possível trabalhar com memória partilhada sem qualquer protecção do sistema de tipos. Além disso comprometem-se com uma memória com um espaço global de endereços único, em contraste com um um espaço global de endereços particionado, não se preparando para sistemas mais esotéricos com modelos de memória diferentes. Para a omissão de excepções é dada a justificação que não é um problema bem resolvido, ignorando o bom trabalho desenvolvido na linguagem X10. Para a concorrência é feito o inverso: albergam ambos os idiomas e promovem timidamente a concorrência por passagem de mensagem.