A linguagem de programação Go tem se tornado cada vez mais popular. Inúmeras empresas estão usando Go para construir infraestrutura de back-end moderna e escalável.
Se você quer aprender uma nova linguagem de programação, Go é uma ótima escolha. É rápida, leve, tem uma comunidade de código aberto incrível e é bem fácil de começar.
Este é um manual totalmente gratuito, baseado em texto. Se quiser começar, basta rolar a página para baixo e começar a ler! Dito isso, há duas outras opções para acompanhar.
-
Experimente a versão interativa deste curso de Golang no Boot.dev , completo com desafios e projetos de codificação
Índice
-
Por que aprender Go?
-
Como compilar código Go
-
Variáveis em Go
-
Funções em Go
-
Estruturas em Go
-
Interfaces em Go
-
Erros em Go
-
Loops em Go
-
Matrizes e fatias em Go
-
Mapas em Go
-
Funções avançadas em Go
-
Ponteiros em Go
-
Desenvolvimento Local em Go
-
Canais em Go
-
Mutexes em Go
-
Genéricos em Go
Capítulo 1 – Por que aprender Go?
Go é rápido, simples e produtivo. Go é uma das linguagens de programação mais rápidas, superando JavaScript, Python e Ruby com folga na maioria dos benchmarks.
Mas o código em Go não roda tão rápido quanto seus equivalentes compilados em Rust e C. Dito isso, ele compila muito mais rápido do que eles, o que torna a experiência do desenvolvedor superprodutiva. Infelizmente, não há duelos entre equipes de Go...
Quadrinhos de xkcd
Go tem crescido muito no setor de desenvolvimento backend , então se você tem interesse em conseguir um emprego como desenvolvedor backend , Go pode ser uma ótima opção de tecnologia para adicionar ao seu kit de ferramentas.
Como baixar e instalar o Go Toolchain
Normalmente recomendo uma de duas maneiras:
Certifique-se de usar pelo menos a versão 1.20
. Você pode verificar isso após a instalação digitando:
go version
Uma nota sobre a estrutura de um programa Go
Abordaremos tudo isso com mais detalhes mais tarde, mas para saciar sua curiosidade por enquanto, aqui estão algumas informações sobre o código:
package main
import "fmt"
func main() {
fmt.Println("hello world")
}
-
package main
permite que o compilador Go saiba que queremos que esse código seja compilado e executado como um programa independente, em vez de ser uma biblioteca importada por outros programas. -
import fmt
importa ofmt
pacote (formatação). O pacote de formatação existe na biblioteca padrão do Go e nos permite fazer coisas como imprimir texto no console. -
func main()
define amain
função.main
é o nome da função que atua como ponto de entrada para um programa Go.
Salve o código acima em um arquivo chamado main.go
, execute go build
e depois execute o executável resultante.
go build -o out
./out
Você também pode usar o playground Go do Bootdev para testar todos os trechos deste curso diretamente no seu navegador.
Capítulo 2 – Como compilar código Go
O que significa ser compilado?
Computadores precisam de código de máquina – eles não entendem inglês nem Go. Precisamos converter nosso código de alto nível (Go) para linguagem de máquina, que é, na verdade, apenas um conjunto de instruções que um hardware específico consegue entender. No seu caso, sua CPU.
A função do compilador Go é pegar código Go e produzir código de máquina. No Windows, seria um .exe
arquivo. No Mac ou Linux, seria qualquer arquivo executável.
Os computadores não sabem fazer nada a menos que nós, programadores, lhes digamos o que fazer. Infelizmente, os computadores não entendem a linguagem humana. Aliás, eles nem sequer entendem programas de computador não compilados.
Por exemplo, o código:
package main
import "fmt"
func main(){
fmt.Println("hello world")
}
não significa nada para um computador.
Os computadores precisam de código de máquina
A CPU de um computador entende apenas seu próprio conjunto de instruções , que chamamos de "código de máquina". Instruções são operações matemáticas básicas, como adição, subtração, multiplicação e a capacidade de salvar dados temporariamente.
Por exemplo, um processador ARM usa a instrução ADD quando fornecido com o número 0100
em binário.
Go, C, Rust e assim por diante
Go, C e Rust são linguagens em que o código é primeiro convertido em código de máquina pelo compilador antes de ser executado.
Compilado vs. Interpretado
Programas compilados podem ser executados sem acesso ao código-fonte original e sem acesso a um compilador.
Por exemplo, quando seu navegador executa o código que você escreveu neste curso, ele não usa o código original, apenas o resultado compilado. Observe como isso é diferente de linguagens interpretadas como Python e JavaScript.
Com Python e JavaScript, o código é interpretado em tempo de execução por um programa separado conhecido como "interpretador". Distribuir código para os usuários executarem pode ser um problema, pois eles precisam ter um interpretador instalado e acessar o código-fonte original.
Exemplos de linguagens compiladas
-
Ir
-
C
-
C++
-
Ferrugem
Exemplos de linguagens interpretadas
-
JavaScript
-
Pitão
-
Rubi
Ilustração de linguagens compiladas vs interpretadas
Go é fortemente tipado
Go impõe tipagem forte e estática, o que significa que variáveis só podem ter um único tipo. Uma string
variável como "hello world" não pode ser alterada para um tipo int
, como o número 3
.
Um dos maiores benefícios da tipagem forte é que erros podem ser detectados em "tempo de compilação". Em outras palavras, bugs são mais facilmente detectados com antecedência, pois são detectados quando o código é compilado, antes mesmo de ser executado.
Compare isso com a maioria das linguagens interpretadas, onde os tipos de variáveis são dinâmicos. A tipagem dinâmica pode levar a bugs sutis e difíceis de detectar. Com linguagens interpretadas, o código precisa ser executado (às vezes em produção, se você não tiver sorte 😨) para detectar erros de sintaxe e de tipo.
Por exemplo, o código a seguir não será compilado porque strings e ints não podem ser somados:
func main() {
var username string = "wagslane"
var password int = 20947382822
// don't edit below this line
fmt.Println("Authorization: Basic", username+":"+password)
}
Os programas Go são leves
Programas em Go são bastante leves. Cada programa inclui uma pequena quantidade de código "extra" que é incluído no binário executável. Esse código extra é chamado de Tempo de Execução Go . Um dos propósitos do tempo de execução Go é limpar a memória não utilizada em tempo de execução.
Em outras palavras, o compilador Go inclui uma pequena quantidade de lógica extra em cada programa Go para facilitar aos desenvolvedores a escrita de código que seja eficiente em termos de memória.
Como regra geral, programas Java usam mais memória do que programas Go comparáveis, pois Go não utiliza uma máquina virtual inteira para executar seus programas, apenas um pequeno tempo de execução. O tempo de execução de Go é pequeno o suficiente para ser incluído diretamente no código de máquina compilado de cada programa Go.
Como regra geral, programas em Rust e C++ usam um pouco menos de memória do que programas em Go, pois o desenvolvedor tem mais controle para otimizar o uso de memória do programa. O tempo de execução em Go cuida disso automaticamente para nós.
Gráfico mostrando a comparação do uso de memória ociosa entre Java (162 MB), Go (0,86 MB) e Rust (0,36 MB)
No gráfico acima, Dexter Darwich compara o uso de memória de três programas muito simples escritos em Java, Go e Rust. Como você pode ver, Go e Rust usam pouquíssima memória em comparação com Java.
Capítulo 3 – Variáveis em Go
Os tipos básicos de variáveis do Go são:
bool
string
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
byte // alias for uint8
rune // alias for int32
// represents a Unicode code point
float32 float64
complex64 complex128
Falamos sobre string
s e int
s anteriormente, e esses dois tipos devem ser bastante autoexplicativos.
A bool
é uma variável booleana, o que significa que tem o valor de true
ou false
. Os tipos de ponto flutuante ( float32
e float64
) são usados para números que não são inteiros – ou seja, têm dígitos à direita da casa decimal, como 3.14159
. O float32
tipo usa 32 bits de precisão, enquanto o float64
tipo usa 64 bits para poder armazenar mais dígitos com mais precisão.
Não se preocupe muito com as complexidades dos outros tipos por enquanto. Abordaremos alguns deles com mais detalhes à medida que avançarmos.
Como declarar uma variável
Variáveis são declaradas usando a var
palavra-chave . Por exemplo, para declarar uma variável chamada number
do tipo int
, você escreveria:
var number int
Para declarar uma variável chamada pi
para ser do tipo float64
com um valor de 3.14159
, você escreveria:
var pi float64 = 3.14159
O valor de uma variável inicializada sem atribuição será seu valor zero .
Declaração de Variável Curta
Dentro de uma função (mesmo a função principal), a :=
instrução de atribuição curta pode ser usada no lugar de uma var
declaração. O :=
operador infere o tipo da nova variável com base no valor.
var empty string
É o mesmo que:
empty := ""
numCars := 10 // inferred to be an integer
temperature := 0.0 // temperature is inferred to be a floating point value because it has a decimal point
var isFunny = true // isFunny is inferred to be a boolean
Fora de uma função (no escopo global/pacote ), cada instrução começa com uma palavra-chave ( var
, func
, e assim por diante) e, portanto, a :=
construção não está disponível.
Inferência de Tipo
Para declarar uma variável sem especificar um tipo explícito (usando a :=
sintaxe ou var = expression
sintaxe), o tipo da variável é inferido a partir do valor no lado direito.
Quando o lado direito da declaração é digitado, a nova variável é do mesmo tipo:
var i int
j := i // j is also an int
Entretanto, quando o lado direito é um valor literal (uma constante numérica não tipada como 42
ou 3.14
), a nova variável será um int
, float64
, ou complex128
dependendo de sua precisão:
i := 42 // int
f := 3.14 // float64
g := 0.867 + 0.5i // complex128
Declarações da mesma linha
Podemos declarar múltiplas variáveis na mesma linha:
mileage, company := 80276, "Tesla"
// is the same as
mileage := 80276
company := "Tesla"
Tamanhos de tipo
Ints, uints , floats e números complexos têm tamanhos de tipo.
int int8 int16 int32 int64 // whole numbers
uint uint8 uint16 uint32 uint64 uintptr // positive whole numbers
float32 float64 // decimal numbers
complex64 complex128 // imaginary numbers (rare)
O tamanho (8, 16, 32, 64, 128 e assim por diante) indica quantos bits de memória serão usados para armazenar a variável. Os tipos padrão int
e uint
são apenas aliases que se referem aos seus respectivos tamanhos de 32 ou 64 bits, dependendo do ambiente do usuário.
Os tamanhos padrão que devem ser usados, a menos que você tenha uma necessidade específica, são:
-
int
-
uint
-
float64
-
complex128
Alguns tipos podem ser convertidos da seguinte maneira:
temperatureInt := 88
temperatureFloat := float64(temperatureInt)
Converter um float em um inteiro dessa forma trunca a parte do ponto flutuante.
Que tipo devo usar?
Com tantos tipos para o que é essencialmente apenas um número, desenvolvedores que vêm de linguagens que têm apenas um tipo de Number
tipo (como JavaScript) podem achar as escolhas assustadoras.
Um problema surge quando temos um uint16
, e a função para a qual estamos tentando passá-lo recebe um int
. Somos forçados a escrever código cheio de conversões de tipo como int(myUint16)
.
Esse estilo de desenvolvimento pode ser lento e incômodo de ler. Quando desenvolvedores de Go se desviam do tipo "padrão" para qualquer família de tipos, o código pode ficar confuso rapidamente.
A menos que você tenha um bom motivo, opte pelos seguintes tipos:
-
bool
-
string
-
int
-
uint
-
byte
-
rune
-
float64
-
complex128
Constantes
Constantes são declaradas como variáveis, mas usam a const
palavra-chave . Constantes não podem usar a :=
sintaxe de declaração curta.
Constantes podem ser caracteres, strings, valores booleanos ou numéricos. Não podem ser tipos mais complexos, como slices, mapas e structs, que são tipos que explicarei mais adiante.
Como o nome indica, o valor de uma constante não pode ser alterado depois de ter sido declarada.
As constantes devem ser conhecidas em tempo de compilação . Na maioria das vezes, elas serão declaradas com um valor estático:
const myInt = 15
No entanto, constantes podem ser computadas , desde que o cálculo possa ocorrer em tempo de compilação. Por exemplo, isto é válido:
const firstName = "Lane"
const lastName = "Wagner"
const fullName = firstName + " " + lastName
Dito isto, você não pode declarar uma constante que só pode ser computada em tempo de execução.
Como formatar strings em Go
Go segue a tradição printf da linguagem C. Na minha opinião, a formatação/interpolação de strings em Go é atualmente menos elegante que JavaScript e Python.
-
fmt.Printf – Imprime uma string formatada na saída padrão
-
fmt.Sprintf() – Retorna a string formatada
Exemplos
Esses verbos de formatação funcionam com fmt.Printf
e fmt.Sprintf
.
%v
- Interpolar a representação padrão
A %v
variante imprime a representação da sintaxe Go de um valor. Geralmente, você pode usá-la se não tiver certeza do que mais usar. Dito isso, é melhor usar a variante específica do tipo, se possível.
s := fmt.Sprintf("I am %v years old", 10)
// I am 10 years old
s := fmt.Sprintf("I am %v years old", "way too many")
// I am way too many years old
%s
- Interpolar uma string
s := fmt.Sprintf("I am %s years old", "way too many")
// I am way too many years old
%d
- Interpolar um inteiro na forma decimal
s := fmt.Sprintf("I am %d years old", 10)
// I am 10 years old
%f
- Interpolar um decimal
s := fmt.Sprintf("I am %f years old", 10.523)
// I am 10.523000 years old
// The ".2" rounds the number to 2 decimal places
s := fmt.Sprintf("I am %.2f years old", 10.523)
// I am 10.53 years old
Se você estiver interessado em todas as opções de formatação, sinta-se à vontade para dar uma olhada na documentação fmt
do pacote aqui .
Condicionais
if
instruções em Go não usam parênteses ao redor da condição:
if height > 4 {
fmt.Println("You are tall enough!")
}
else if
e else
são suportados conforme você esperaria:
if height > 6 {
fmt.Println("You are super tall!")
} else if height > 4 {
fmt.Println("You are tall enough!")
} else {
fmt.Println("You are not tall enough!")
}
A declaração inicial de um bloco if
Uma if
condicional pode ter uma instrução "inicial". As variáveis criadas na instrução inicial são definidas apenas dentro do escopo do if
corpo.
if INITIAL_STATEMENT; CONDITION {
}
Isto é apenas um pouco de sintaxe que Go oferece para encurtar o código em alguns casos. Por exemplo, em vez de escrever:
length := getLength(email)
if length < 1 {
fmt.Println("Email is invalid")
}
Nós podemos fazer:
if length := getLength(email); length < 1 {
fmt.Println("Email is invalid")
}
Além de ser um pouco mais curto, esse código também o remove length
do escopo pai. Isso é conveniente porque não precisamos dele lá – só precisamos acessá-lo durante a verificação de uma condição.
Capítulo 4 – Funções em Go
Funções em Go podem receber zero ou mais argumentos.
Para facilitar a leitura do código Go, o tipo de variável vem depois do nome da variável.
Por exemplo, a seguinte função:
func sub(x int, y int) int {
return x-y
}
Aceita dois parâmetros inteiros e retorna outro inteiro.
Aqui, func sub(x int, y int) int
é conhecida como "assinatura de função".
Parâmetros múltiplos
Quando vários argumentos são do mesmo tipo, o tipo só precisa ser declarado depois do último, supondo que estejam em ordem.
Por exemplo:
func add(x, y int) int {
return x + y
}
Se não estiverem em ordem, precisam ser definidos separadamente.
Sintaxe de Declaração de Função
Os desenvolvedores frequentemente se perguntam por que a sintaxe de declaração em Go é diferente da tradição estabelecida na família de linguagens C.
Sintaxe estilo C
A linguagem C descreve tipos com uma expressão que inclui o nome a ser declarado e indica qual tipo essa expressão terá.
int y;
O código acima declara y
como int
. Em geral, o tipo fica à esquerda e a expressão à direita.
Curiosamente, os criadores da linguagem Go concordaram que o estilo C de declarar tipos em assinaturas se torna confuso muito rápido – dê uma olhada neste pesadelo.
int (*fp)(int (*ff)(int x, int y), int b)
Sintaxe estilo Go
As declarações de Go são claras, você apenas as lê da esquerda para a direita, como faria em inglês.
x int
p *int
a [3]int
É bom para assinaturas mais complexas, pois as torna mais fáceis de ler.
f func(func(int,int) int, int) int
Como Passar Variáveis por Valor
Variáveis em Go são passadas por valor (exceto por alguns tipos de dados que ainda não abordamos). "Passagem por valor" significa que, quando uma variável é passada para uma função, essa função recebe uma cópia da variável. A função não consegue alterar os dados originais de quem a chamou.
func main(){
x := 5
increment(x)
fmt.Println(x)
// still prints 5,
// because the increment function
// received a copy of x
}
func increment(x int){
x++
}
Como ignorar valores de retorno
Uma função pode retornar um valor que não interessa ao chamador. Podemos ignorar variáveis explicitamente usando um sublinhado:_
Por exemplo:
func getPoint() (x int, y int) {
return 3, 4
}
// ignore y value
x, _ := getPoint()
Mesmo que getPoint()
retorne dois valores, podemos capturar o primeiro e ignorar o segundo.
Por que você ignoraria um valor de retorno?
Pode haver muitos motivos. Por exemplo, talvez uma função chamada getCircle
retorne o ponto central e o raio, mas você só precisa do raio para o seu cálculo. Nesse caso, você ignoraria a variável do ponto central.
É crucial entender isso porque o compilador Go lançará um erro se você tiver declarações de variáveis não utilizadas no seu código, então você precisa ignorar qualquer coisa que não pretenda usar.
Valores de retorno nomeados
Os valores de retorno podem receber nomes e, se receberem, serão tratados da mesma forma como se fossem novas variáveis definidas no topo da função.
Valores de retorno nomeados são melhor considerados como uma forma de documentar a finalidade dos valores retornados.
De acordo com o tour de go :
Uma instrução return sem argumentos retorna os valores de retorno nomeados. Isso é conhecido como um return "nu". Instruções return nuas devem ser usadas apenas em funções curtas. Elas podem prejudicar a legibilidade em funções mais longas.
func getCoords() (x, y int){
// x and y are initialized with zero values
return // automatically returns x and y
}
É o mesmo que:
func getCoords() (int, int){
var x int
var y int
return x, y
}
No primeiro exemplo, x
e y
são os valores de retorno. No final da função, poderíamos simplesmente escrever return
para retornar os valores dessas duas variáveis, em vez de escrever return x,y
.
Retornos explícitos
Mesmo que uma função tenha valores de retorno nomeados, ainda podemos retornar valores explicitamente se quisermos.
func getCoords() (x, y int){
return x, y // this is explicit
}
Usando esse padrão explícito, podemos até mesmo sobrescrever os valores de retorno:
func getCoords() (x, y int){
return 5, 6 // this is explicit, x and y are NOT returned
}
Caso contrário, se quisermos retornar os valores definidos na assinatura da função, podemos simplesmente usar um return
retorno nu (em branco):
func getCoords() (x, y int){
return // implicitly returns x and y
}
Os benefícios dos retornos nomeados
-
Bom para documentação (compreensão)
Parâmetros de retorno nomeados são ótimos para documentar uma função. Sabemos o que a função está retornando diretamente pela sua assinatura, sem necessidade de comentários.
Parâmetros de retorno nomeados são particularmente importantes em funções mais longas com muitos valores de retorno.
func calculator(a, b int) (mul, div int, err error) {
if b == 0 {
return 0, 0, errors.New("Can't divide by zero")
}
mul = a * b
div = a / b
return mul, div, nil
}
O que é mais fácil de entender do que:
func calculator(a, b int) (int, int, error) {
if b == 0 {
return 0, 0, errors.New("Can't divide by zero")
}
mul := a * b
div := a / b
return mul, div, nil
}
Sabemos o significado de cada valor de retorno apenas observando a assinatura da função:func calculator(a, b int) (mul, div int, err error)
Menos código (às vezes)
Se houver várias instruções de retorno em uma função, você não precisa escrever todos os valores de retorno de cada vez, embora provavelmente devesse.
Quando você omite valores de retorno, isso é chamado de retorno nulo . Retornos nus devem ser usados apenas em funções curtas e simples.
Retornos antecipados
Go suporta a capacidade de retornar antecipadamente de uma função. Este é um recurso poderoso que pode limpar o código, especialmente quando usado como cláusulas de guarda .
Cláusulas de Guarda aproveitam a capacidade de retornar return
antecipadamente de uma função (ou continue
através de um loop) para tornar condicionais aninhados unidimensionais. Em vez de usar cadeias if/else, simplesmente retornamos antecipadamente da função ao final de cada bloco condicional.
func divide(dividend, divisor int) (int, error) {
if divisor == 0 {
return 0, errors.New("Can't divide by zero")
}
return dividend/divisor, nil
}
O tratamento de erros em Go naturalmente incentiva os desenvolvedores a usar cláusulas de proteção. Quando comecei a escrever mais JavaScript, fiquei decepcionado ao ver quantas condicionais aninhadas existiam no código em que eu estava trabalhando.
Vamos dar uma olhada em um exemplo exagerado de lógica condicional aninhada:
func getInsuranceAmount(status insuranceStatus) int {
amount := 0
if !status.hasInsurance(){
amount = 1
} else {
if status.isTotaled(){
amount = 10000
} else {
if status.isDented(){
amount = 160
if status.isBigDent(){
amount = 270
}
} else {
amount = 0
}
}
}
return amount
}
Isso poderia ser escrito com cláusulas de guarda:
func getInsuranceAmount(status insuranceStatus) int {
if !status.hasInsurance(){
return 1
}
if status.isTotaled(){
return 10000
}
if !status.isDented(){
return 0
}
if status.isBigDent(){
return 270
}
return 160
}
O exemplo acima é muito mais fácil de ler e entender. Ao escrever código, é importante tentar reduzir a carga cognitiva do leitor, reduzindo o número de entidades nas quais ele precisa pensar a qualquer momento.
No primeiro exemplo, se o desenvolvedor estiver tentando descobrir se when
270 foi retornado, ele precisa pensar em cada ramificação na árvore lógica e tentar lembrar quais casos são importantes e quais não.
Com a estrutura unidimensional oferecida pelas cláusulas de guarda, é tão simples quanto percorrer cada caso em ordem.
Capítulo 5 – Estruturas em Go
Usamos structs em Go para representar dados estruturados. Muitas vezes, é conveniente agrupar diferentes tipos de variáveis. Por exemplo, se quisermos representar um carro, podemos fazer o seguinte:
type car struct {
Make string
Model string
Height int
Width int
}
Isso cria um novo tipo de estrutura chamado car
. Todos os carros têm um Make
, Model
, Height
e Width
.
Em Go, você frequentemente usará uma struct para representar informações para as quais usaria um dicionário em Python, ou um literal de objeto em JavaScript.
Estruturas aninhadas em Go
Estruturas podem ser aninhadas para representar entidades mais complexas:
type car struct {
Make string
Model string
Height int
Width int
FrontWheel Wheel
BackWheel Wheel
}
type Wheel struct {
Radius int
Material string
}
Os campos de uma struct podem ser acessados usando o .
operador ponto.
myCar := car{}
myCar.FrontWheel.Radius = 5
Estruturas Anônimas
Uma struct anônima é como uma struct normal, mas é definida sem um nome e, portanto, não pode ser referenciada em nenhum outro lugar no código.
Para criar uma struct anônima, basta instanciar a instância imediatamente usando um segundo par de colchetes após declarar o tipo:
myCar := struct {
Make string
Model string
} {
Make: "tesla",
Model: "model 3"
}
Você pode até aninhar estruturas anônimas como campos dentro de outras estruturas:
type car struct {
Make string
Model string
Height int
Width int
// Wheel is a field containing an anonymous struct
Wheel struct {
Radius int
Material string
}
}
Quando você deve usar uma estrutura anônima?
Em geral, prefira structs nomeadas . structs nomeadas facilitam a leitura e a compreensão do seu código e têm o ótimo efeito colateral de serem reutilizáveis. Às vezes, uso structs anônimas quando sei que nunca mais precisarei usar uma struct. Por exemplo, às vezes uso uma para criar o formato de alguns dados JSON em manipuladores HTTP.
Se uma struct deve ser usada apenas uma vez, faz sentido declará-la de forma que os desenvolvedores no futuro não fiquem tentados a usá-la acidentalmente novamente.
Você pode ler mais sobre estruturas anônimas aqui se estiver curioso.
Estruturas Embutidas
Go não é uma linguagem orientada a objetos . Mas estruturas incorporadas fornecem um tipo de herança somente de dados que pode ser útil às vezes.
Lembre-se de que Go não suporta classes ou herança em seu sentido completo. Estruturas incorporadas são apenas uma maneira de elevar e compartilhar campos entre definições de estruturas.
type car struct {
make string
model string
}
type truck struct {
// "car" is embedded, so the definition of a
// "truck" now also additionally contains all
// of the fields of the car struct
car
bedSize int
}
Incorporado vs. aninhado
-
Os campos de uma estrutura incorporada são acessados no nível superior, diferentemente das estruturas aninhadas.
-
Os campos promovidos podem ser acessados como campos normais, exceto que não podem ser usados em literais compostos
lanesTruck := truck{
bedSize: 10,
car: car{
make: "toyota",
model: "camry",
},
}
fmt.Println(lanesTruck.bedSize)
// embedded fields promoted to the top-level
// instead of lanesTruck.car.make
fmt.Println(lanesTruck.make)
fmt.Println(lanesTruck.model)
Métodos de Estrutura
Embora Go não seja orientado a objetos, ele suporta métodos que podem ser definidos em structs. Métodos são apenas funções que possuem um receptor. Um receptor é um parâmetro especial que sintaticamente precede o nome da função.
type rect struct {
width int
height int
}
// area has a receiver of (r rect)
func (r rect) area() int {
return r.width * r.height
}
r := rect{
width: 5,
height: 10,
}
fmt.Println(r.area())
// prints 50
Um receptor é apenas um tipo especial de parâmetro de função. Receptores são importantes porque, como você aprenderá nos próximos exercícios, eles nos permitirão definir interfaces que nossas estruturas (e outros tipos) podem implementar.
Capítulo 6 – Interfaces em Go
Interfaces são coleções de assinaturas de métodos. Um tipo "implementa" uma interface se tiver todos os métodos da interface fornecida definidos nele.
No exemplo a seguir, uma "forma" deve ser capaz de retornar sua área e perímetro. Ambos rect
devem circle
preencher a interface.
type shape interface {
area() float64
perimeter() float64
}
type rect struct {
width, height float64
}
func (r rect) area() float64 {
return r.width * r.height
}
func (r rect) perimeter() float64 {
return 2*r.width + 2*r.height
}
type circle struct {
radius float64
}
func (c circle) area() float64 {
return math.Pi * c.radius * c.radius
}
func (c circle) perimeter() float64 {
return 2 * math.Pi * c.radius
}
Quando um tipo implementa uma interface, ele pode ser usado como o tipo de interface.
As interfaces são implementadas implicitamente .
Um tipo nunca declara que implementa uma interface específica. Se uma interface existe e um tipo possui os métodos apropriados definidos, então o tipo automaticamente atende a essa interface.
Interfaces múltiplas
Um tipo pode implementar qualquer número de interfaces em Go. Por exemplo, a interface vazia, interface{}
, é sempre implementada por todos os tipos porque não possui requisitos.
Nomeando argumentos de interface
Considere a seguinte interface:
type Copier interface {
Copy(string, string) int
}
Com base apenas no código, você consegue deduzir que tipos de strings você deve passar para a Copy
função?
Sabemos que a assinatura da função espera dois tipos de string, mas quais são eles? Nomes de arquivo? URLs? Dados de string brutos? Aliás, o que diabos é isso int
que está sendo retornado?
Vamos adicionar alguns argumentos nomeados e retornar dados para deixar mais claro.
type Copier interface {
Copy(sourceFile string, destinationFile string) (bytesCopied int)
}
Muito melhor. Agora podemos ver quais são as expectativas. O primeiro argumento é o sourceFile
, o segundo argumento é o destinationFile
, e bytesCopied
, um inteiro, é retornado.
Asserções de tipo em Go
Ao trabalhar com interfaces em Go, ocasionalmente você precisará acessar o tipo subjacente de um valor de interface. Você pode converter uma interface para seu tipo subjacente usando uma asserção de tipo .
type shape interface {
area() float64
}
type circle struct {
radius float64
}
// "c" is a new circle cast from "s"
// which is an instance of a shape.
// "ok" is a bool that is true if s was a circle
// or false if s isn't a circle
c, ok := s.(circle)
if !ok {
// s wasn't a circle
log.Fatal("s is not a circle")
}
radius := c.radius
Trocas de tipo em Go
Uma troca de tipo facilita a execução de diversas asserções de tipo em uma série.
Uma troca de tipo é semelhante a uma instrução switch regular, mas os casos especificam tipos em vez de valores .
func printNumericValue(num interface{}) {
switch v := num.(type) {
case int:
fmt.Printf("%T
", v)
case string:
fmt.Printf("%T
", v)
default:
fmt.Printf("%T
", v)
}
}
func main() {
printNumericValue(1)
// prints "int"
printNumericValue("1")
// prints "string"
printNumericValue(struct{}{})
// prints "struct {}"
}
fmt.Printf("%T
", v)
imprime o tipo de uma variável.
Interfaces limpas
Escrever interfaces limpas é difícil . Francamente, sempre que você lida com abstrações em código, o simples pode se tornar complexo muito rapidamente se você não tomar cuidado. Vamos rever algumas regras básicas para manter interfaces limpas .
1. Mantenha as interfaces pequenas
Se você pudesse tirar apenas um conselho deste artigo, seria este: mantenha as interfaces pequenas! As interfaces têm como objetivo definir o comportamento mínimo necessário para representar com precisão uma ideia ou conceito.
Aqui está um exemplo do pacote HTTP padrão de uma interface maior que é um bom exemplo de definição de comportamento mínimo:
type File interface {
io.Closer
io.Reader
io.Seeker
Readdir(count int) ([]os.FileInfo, error)
Stat() (os.FileInfo, error)
}
Qualquer tipo que satisfaça os comportamentos da interface pode ser considerado pelo pacote HTTP como um File . Isso é conveniente porque o pacote HTTP não precisa saber se está lidando com um arquivo em disco, um buffer de rede ou um simples []byte
.
2. As interfaces não devem ter conhecimento de tipos satisfatórios
Uma interface deve definir o que é necessário para que outros tipos sejam classificados como membros dessa interface. Eles não devem estar cientes de nenhum tipo que satisfaça a interface em tempo de design.
Por exemplo, vamos supor que estamos construindo uma interface para descrever os componentes necessários para definir um carro.
type car interface {
Color() string
Speed() int
IsFiretruck() bool
}
Color()
e Speed()
fazem todo o sentido, são métodos restritos ao escopo de um carro. IsFiretruck()
é um antipadrão. Estamos forçando todos os carros a declarar se são ou não caminhões de bombeiros. Para que esse padrão faça algum sentido, precisaríamos de uma lista completa de subtipos possíveis. IsPickup()
, IsSedan()
, IsTank()
… onde isso termina??
Em vez disso, o desenvolvedor deveria ter se baseado na funcionalidade nativa de asserção de tipo para derivar o tipo subjacente ao receber uma instância da interface car. Ou, se uma subinterface for necessária, ela pode ser definida como:
type firetruck interface {
car
HoseLength() int
}
Que herda os métodos necessários car
e adiciona um método necessário adicional para fazer o car
a firetruck
.
3. Interfaces não são classes
-
Interfaces não são classes, elas são mais enxutas.
-
As interfaces não têm construtores ou desconstrutores que exijam que dados sejam criados ou destruídos.
-
Interfaces não são hierárquicas por natureza, embora haja um recurso sintático para criar interfaces que são superconjuntos de outras interfaces.
-
Interfaces definem assinaturas de funções, mas não o comportamento subjacente. Criar uma interface geralmente não esgota seu código em relação aos métodos de struct. Por exemplo, se cinco tipos satisfazem a
fmt.Stringer
interface, todos eles precisam de sua própria versão daString()
função.
Capítulo 7 – Erros em Go
Programas em Go expressam erros com valores. Um erro é qualquer tipo que implemente a interface de erroerror
integrada simples :
type error interface {
Error() string
}
Quando algo pode dar errado em uma função, essa função deve retornar um error
como seu último valor de retorno. Qualquer código que chame uma função que possa retornar um error
deve lidar com erros testando se o erro é nil
.
// Atoi converts a stringified number to an interger
i, err := strconv.Atoi("42b")
if err != nil {
fmt.Println("couldn't convert:", err)
// because "42b" isn't a valid integer, we print:
// couldn't convert: strconv.Atoi: parsing "42b": invalid syntax
// Note:
// 'parsing "42b": invalid syntax' is returned by the .Error() method
return
}
// if we get here, then
// i was converted successfully
Um nil
erro denota sucesso. Um erro não nulo denota falha.
A interface de erro
Como erros são apenas interfaces, você pode criar seus próprios tipos personalizados que implementam a error
interface. Aqui está um exemplo de uma userError
struct que implementa a error
interface:
type userError struct {
name string
}
func (e userError) Error() string {
return fmt.Sprintf("%v has a problem with their account", e.name)
}
Ele pode então ser usado como um erro:
func sendSMS(msg, userName string) error {
if !canSendToUser(userName) {
return userError{name: userName}
}
...
}
Programas em Go expressam erros com valores. Valores de erro são quaisquer tipos que implementam a interface de erroerror
integrada simples .
Lembre-se de que a maneira como Go lida com erros é bastante única. A maioria das linguagens trata erros como algo especial e diferente. Por exemplo, Python gera tipos de exceção e JavaScript lança e captura erros.
Em Go, "an" error
é apenas mais um valor que tratamos como qualquer outro valor – como quisermos! Não há palavras-chave específicas para lidar com eles.
O pacote de erros
A biblioteca padrão Go fornece um pacote "errors" que facilita lidar com erros.
Leia o godoc para a função errors.New() , mas aqui está um exemplo simples:
var err error = errors.New("something went wrong")
Capítulo 8 – Loops em Go
O loop básico em Go é escrito em sintaxe padrão semelhante à C:
for INITIAL; CONDITION; AFTER{
// do something
}
INITIAL
é executado uma vez no início do loop e pode criar variáveis dentro do escopo do loop.
CONDITION
é verificado antes de cada iteração. Se a condição não for atendida, o loop é interrompido.
AFTER
é executado após cada iteração.
Por exemplo:
for i := 0; i < 10; i++ {
fmt.Println(i)
}
// Prints 0 through 9
Como omitir condições
Laços em Go podem omitir seções de um laço for. Por exemplo, a CONDITION
(parte do meio) pode ser omitida, o que faz com que o laço seja executado para sempre.
for INITIAL; ; AFTER {
// do something forever
}
Sem loops while em Go
A maioria das linguagens de programação tem o conceito de while
loop. Como Go permite a omissão de seções de um for
loop, um while
loop é apenas um for
loop que possui apenas uma CONDIÇÃO.
for CONDITION {
// do some stuff while CONDITION is true
}
Por exemplo:
plantHeight := 1
for plantHeight < 5 {
fmt.Println("still growing! current height:", plantHeight)
plantHeight++
}
fmt.Println("plant has grown to ", plantHeight, "inches")
Quais impressões:
still growing! current height: 1
still growing! current height: 2
still growing! current height: 3
still growing! current height: 4
plant has grown to 5 inches
Continuar através de um loop
A continue
palavra-chave interrompe a iteração atual de um loop e continua para a próxima iteração. continue
é uma maneira poderosa de usar o padrão "cláusula de guarda" dentro de loops.
for i := 0; i < 10; i++ {
if i % 2 == 0 {
continue
}
fmt.Println(i)
}
// 1
// 3
// 5
// 7
// 9
Sair de um loop
A break
palavra-chave interrompe a iteração atual de um loop e sai do loop.
for i := 0; i < 10; i++ {
if i == 5 {
break
}
fmt.Println(i)
}
// 0
// 1
// 2
// 3
// 4
Capítulo 9 – Arrays e Slices em Go
Matrizes
Matrizes são grupos de tamanho fixo de variáveis do mesmo tipo.
O tipo [n]T
é uma matriz de n valores do tipo T
.
Para declarar uma matriz de 10 inteiros:
var myInts [10]int
ou para declarar um literal inicializado:
primes := [6]int{2, 3, 5, 7, 11, 13}
Fatias
99 em cada 100 vezes você usará uma fatia em vez de uma matriz ao trabalhar com listas ordenadas.
Matrizes têm tamanho fixo. Depois de criar uma matriz como [10]int
essa, não é possível adicionar um 11º elemento.
Uma fatia é uma visão flexível e de tamanho dinâmico dos elementos de uma matriz.
Fatias sempre têm um array subjacente, embora isso nem sempre seja especificado explicitamente. Para criar explicitamente uma fatia sobre um array, podemos fazer:
primes := [6]int{2, 3, 5, 7, 11, 13}
mySlice := primes[1:4]
// mySlice = {3, 5, 7}
A sintaxe é:
arrayname[lowIndex:highIndex]
arrayname[lowIndex:]
arrayname[:highIndex]
arrayname[:]
Onde lowIndex
é inclusivo e highIndex
é exclusivo
Um lowIndex
ou highIndex
ambos podem ser omitidos para usar o array inteiro naquele lado.
Como criar novas fatias em Go
Na maioria das vezes, não precisamos pensar no array subjacente de uma fatia. Podemos criar uma nova fatia usando a make
função:
// func make([]T, len, cap) []T
mySlice := make([]int, 5, 10)
// the capacity argument is usually omitted and defaults to the length
mySlice := make([]int, 5)
As fatias criadas com make
serão preenchidas com o valor zero do tipo.
Se quisermos criar uma fatia com um conjunto específico de valores, podemos usar um literal de fatia:
mySlice := []string{"I", "love", "go"}
Observe que os colchetes do array não contêm um 3
. Se tivessem, você teria um array em vez de uma fatia.
Comprimento
O comprimento de uma fatia é simplesmente o número de elementos que ela contém. Ele é acessado usando a função interna len()
:
mySlice := []string{"I", "love", "go"}
fmt.Println(len(mySlice)) // 3
Capacidade
A capacidade de uma fatia é o número de elementos no array subjacente, a partir do primeiro elemento da fatia. Ela é acessada usando a função interna cap()
:
mySlice := []string{"I", "love", "go"}
fmt.Println(cap(mySlice)) // 3
De modo geral, a menos que você esteja hiperotimizando o uso de memória do seu programa, não precisa se preocupar com a capacidade de uma fatia, pois ela aumentará automaticamente conforme necessário.
Funções Variádicas
Muitas funções, especialmente aquelas na biblioteca padrão, podem receber um número arbitrário de argumentos finais . Isso é feito usando a sintaxe "..." na assinatura da função.
Uma função variádica recebe os argumentos variádicos como uma fatia.
func concat(strs ...string) string {
final := ""
// strs is just a slice of strings
for str := range strs {
final += str
}
return final
}
func main() {
final := concat("Hello ", "there ", "friend!")
fmt.Println(total)
// Output: Hello there friend!
}
Os familiares fmt.Println() e fmt.Sprintf() são variáveis! fmt.Println()
imprime cada elemento com delimitadores de espaço e uma nova linha no final.
func Println(a ...interface{}) (n int, err error)
Operador de spread
O operador de dispersão nos permite passar uma fatia para uma função variádica. O operador de dispersão consiste em três pontos após a fatia na chamada da função.
func printStrings(strings ...string) {
for i := 0; i < len(strings); i++ {
fmt.Println(strings[i])
}
}
func main() {
names := []string{"bob", "sue", "alice"}
printStrings(names...)
}
Como anexar a uma fatia
A função append interna é usada para adicionar elementos dinamicamente a uma fatia:
func append(slice []Type, elems ...Type) []Type
Se o array subjacente não for grande o suficiente, append()
criará um novo array subjacente e apontará a fatia para ele.
Observe que append()
é variádico. Todos os seguintes são válidos:
slice = append(slice, oneThing)
slice = append(slice, firstThing, secondThing)
slice = append(slice, anotherSlice...)
Como variar em uma fatia no Go
Go fornece açúcar sintático para iterar facilmente sobre elementos de uma fatia:
for INDEX, ELEMENT := range SLICE {
}
Por exemplo:
fruits := []string{"apple", "banana", "grape"}
for i, fruit := range fruits {
fmt.Println(i, fruit)
}
// 0 apple
// 1 banana
// 2 grape
Capítulo 10 – Mapas em Go
Mapas são semelhantes a objetos JavaScript, dicionários Python e hashes Ruby. Mapas são uma estrutura de dados que fornece mapeamento chave->valor.
O valor zero de um mapa é nil
.
Podemos criar um mapa usando um literal ou usando a make()
função:
ages := make(map[string]int)
ages["John"] = 37
ages["Mary"] = 24
ages["Mary"] = 21 // overwrites 24
ages = map[string]int{
"John": 37,
"Mary": 21,
}
A len()
função funciona em um mapa – ela retorna o número total de pares chave/valor.
ages = map[string]int{
"John": 37,
"Mary": 21,
}
fmt.Println(len(ages)) // 2
Mutações de Mapa
Inserir um elemento
m[key] = elem
Obter um elemento
elem = m[key]
Excluir um elemento
delete(m, key)
Verifique se uma chave existe
elem, ok := m[key]
Se key
estiver em m
, então ok
é true
. Se não, ok
é false
.
Se key
não estiver no mapa, então elem
é o valor zero para o tipo de elemento do mapa.
Tipos de chaves de mapa válidas
Qualquer tipo pode ser usado como valor em um mapa, mas as chaves são mais restritivas.
Você pode ler mais na seção a seguir do blog oficial do Go .
Como mencionado anteriormente, as chaves de mapa podem ser de qualquer tipo comparável . A especificação da linguagem define isso precisamente, mas, resumidamente, tipos comparáveis são booleanos, numéricos, strings, ponteiros, canais e interfaces, e structs ou arrays que contêm apenas esses tipos.
Notavelmente ausentes da lista estão fatias, mapas e funções. Esses tipos não podem ser comparados usando == e não podem ser usados como chaves de mapa.
É óbvio que strings, ints e outros tipos básicos devem estar disponíveis como chaves de mapa, mas talvez sejam inesperadas as chaves struct. Struct pode ser usada para chavear dados por múltiplas dimensões.
Por exemplo, este mapa de mapas poderia ser usado para contabilizar acessos a páginas da web por país:
hits := make(map[string]map[string]int)
Este é um mapa de string para (mapa de string para int). Cada chave do mapa externo é o caminho para uma página da web com seu próprio mapa interno. Cada chave do mapa interno é um código de país de duas letras. Esta expressão recupera o número de vezes que um australiano carregou a página de documentação:
n := hits["/doc/"]["au"]
Infelizmente, essa abordagem se torna difícil de manejar ao adicionar dados, pois para qualquer chave externa fornecida você deve verificar se o mapa interno existe e criá-lo, se necessário:
func add(m map[string]map[string]int, path, country string) {
mm, ok := m[path]
if !ok {
mm = make(map[string]int)
m[path] = mm
}
mm[country]++
}
add(hits, "/doc/", "au")
Por outro lado, um design que usa um único mapa com uma chave struct elimina toda essa complexidade:
type Key struct {
Path, Country string
}
hits := make(map[Key]int)
Quando um vietnamita visita a página inicial, incrementar (e possivelmente criar) o contador apropriado é uma frase de uma linha:
hits[Key{"/", "vn"}]++
E é igualmente simples ver quantos suíços leram a especificação:
n := hits[Key{"/ref/spec", "ch"}]
Mapas aninhados
Mapas podem conter mapas, criando uma estrutura aninhada. Por exemplo:
map[string]map[string]int
map[rune]map[string]int
map[int]map[string]map[string]int
Capítulo 11 – Funções avançadas em Go
Funções de primeira classe e de ordem superior
Diz-se que uma linguagem de programação tem "funções de primeira classe" quando as funções nessa linguagem são tratadas como qualquer outra variável.
Por exemplo, em tal linguagem, uma função pode ser passada como um argumento para outras funções, pode ser retornada por outra função e pode ser atribuída como um valor a uma variável.
Uma função que retorna uma função ou aceita uma função como entrada é chamada de Função de Ordem Superior.
Go suporta funções de primeira classe e de ordem superior. Outra maneira de pensar nisso é que uma função é apenas outro tipo – assim como int
s, string
s e bool
s.
Por exemplo, para aceitar uma função como parâmetro:
func add(x, y int) int {
return x + y
}
func mul(x, y int) int {
return x * y
}
// aggregate applies the given math function to the first 3 inputs
func aggregate(a, b, c int, arithmetic func(int, int) int) int {
return arithmetic(arithmetic(a, b), c)
}
func main(){
fmt.Println(aggregate(2,3,4, add))
// prints 9
fmt.Println(aggregate(2,3,4, mul))
// prints 24
}
Função Currying em Go
Currying de funções é a prática de escrever uma função que recebe uma função (ou funções) como entrada e retorna uma nova função.
Por exemplo:
func main() {
squareFunc := selfMath(multiply)
doubleFunc := selfMath(add)
fmt.Println(squareFunc(5))
// prints 25
fmt.Println(doubleFunc(5))
// prints 10
}
func multiply(x, y int) int {
return x * y
}
func add(x, y int) int {
return x + y
}
func selfMath(mathFunc func(int, int) int) func (int) int {
return func(x int) int {
return mathFunc(x, x)
}
}
No exemplo acima, a selfMath
função recebe uma função como parâmetro e retorna uma função que, por sua vez, retorna o valor da execução dessa função de entrada em seu parâmetro.
Palavra-chave adiar
A defer
palavra-chave é um recurso bastante exclusivo do Go. Ela permite que uma função seja executada automaticamente pouco antes do retorno da função que a envolve.
Os argumentos da chamada adiada são avaliados imediatamente, mas a chamada de função não é executada até que a função circundante retorne.
Funções adiadas são normalmente usadas para fechar conexões de banco de dados, manipuladores de arquivos e similares.
Por exemplo:
// CopyFile copies a file from srcName to dstName on the local filesystem.
func CopyFile(dstName, srcName string) (written int64, err error) {
// Open the source file
src, err := os.Open(srcName)
if err != nil {
return
}
// Close the source file when the CopyFile function returns
defer src.Close()
// Create the destination file
dst, err := os.Create(dstName)
if err != nil {
return
}
// Close the destination file when the CopyFile function returns
defer dst.Close()
return io.Copy(dst, src)
}
No exemplo acima, a src.Close()
função não é chamada até depois que CopyFile
ela é chamada, mas imediatamente antes de CopyFile
ela retornar.
Defer é uma ótima maneira de garantir que algo aconteça no final de uma função, mesmo se houver várias instruções return.
Fechamentos
Um fechamento é uma função que referencia variáveis de fora do seu próprio corpo. A função pode acessar e atribuir valores às variáveis referenciadas.
Neste exemplo, a concatter()
função retorna uma função que faz referência a um valor fechado doc
. Cada chamada sucessiva a harryPotterAggregator
altera essa mesma doc
variável.
func concatter() func(string) string {
doc := ""
return func(word string) string {
doc += word + " "
return doc
}
}
func main() {
harryPotterAggregator := concatter()
harryPotterAggregator("Mr.")
harryPotterAggregator("and")
harryPotterAggregator("Mrs.")
harryPotterAggregator("Dursley")
harryPotterAggregator("of")
harryPotterAggregator("number")
harryPotterAggregator("four,")
harryPotterAggregator("Privet")
fmt.Println(harryPotterAggregator("Drive"))
// Mr. and Mrs. Dursley of number four, Privet Drive
}
Funções Anônimas
Funções anônimas são fiéis à sua forma, pois não têm nome . Nós as utilizamos ao longo deste capítulo, mas ainda não falamos sobre elas.
Funções anônimas são úteis ao definir uma função que será usada apenas uma vez ou para criar um fechamento rápido .
// doMath accepts a function that converts one int into another
// and a slice of ints. It returns a slice of ints that have been
// converted by the passed in function.
func doMath(f func(int) int, nums []int) []int {
var results []int
for _, n := range nums {
results = append(results, f(n))
}
return results
}
func main() {
nums := []int{1, 2, 3, 4, 5}
// Here we define an anonymous function that doubles an int
// and pass it to doMath
allNumsDoubled := doMath(func(x int) int {
return x + x
}, nums)
fmt.Println(allNumsDoubled)
// prints:
// [2 4 6 8 10]
}
Capítulo 12 – Ponteiros em Go
Como aprendemos, uma variável é um local nomeado na memória que armazena um valor. Podemos manipular o valor de uma variável atribuindo um novo valor a ela ou realizando operações nela. Quando atribuímos um valor a uma variável, estamos armazenando esse valor em um local específico na memória.
x := 42
// "x" is the name of a in memory. That is storing the integer value of 42
Um ponteiro é uma variável
Um ponteiro é uma variável que armazena o endereço de memória de outra variável. Isso significa que um ponteiro "aponta para" o local onde os dados estão armazenados, NÃO os dados em si.
A *
sintaxe define um ponteiro:
var p *int
O &
operador gera um ponteiro para seu operando.
myString := "hello"
myStringPtr = &myString
Por que os ponteiros são úteis?
Ponteiros nos permitem manipular dados na memória diretamente, sem fazer cópias ou duplicar dados. Isso pode tornar os programas mais eficientes e nos permitir fazer coisas que seriam difíceis ou impossíveis sem eles.
Sintaxe de ponteiro
A *
sintaxe define um ponteiro:
var p *int
O valor zero de um ponteiro énil
O &
operador gera um ponteiro para seu operando:
myString := "hello"
myStringPtr = &myString
Ele *
desreferencia um ponteiro para obter acesso ao valor:
fmt.Println(*myStringPtr) // read myString through the pointer
*myStringPtr = "world" // set myString through the pointer
Ao contrário de C, Go não tem aritmética de ponteiros
Só porque você pode não significa que você deva
Estamos fazendo este exercício para entender que ponteiros podem ser usados dessa maneira. Dito isso, ponteiros podem ser muito perigosos. Geralmente, é melhor que suas funções aceitem não ponteiros e retornem novos valores em vez de alterar entradas de ponteiros.
Ponteiros nulos
Mais uma vez, os ponteiros podem ser muito perigosos.
Se um ponteiro não aponta para nada (o valor zero do tipo de ponteiro), então desreferenciá-lo causará um erro de tempo de execução (um panic ) que trava o programa.
De modo geral, sempre que estiver lidando com ponteiros, você deve verificar se eles estão corretos nil
antes de tentar desreferenciá-los.
Receptores do Método de Ponteiro
Um tipo de receptor em um método pode ser um ponteiro.
Métodos com receptores de ponteiro podem modificar o valor para o qual o receptor aponta. Como os métodos frequentemente precisam modificar seu receptor, receptores de ponteiro são mais comuns do que receptores de valor.
Receptor de ponteiro
type car struct {
color string
}
func (c *car) setColor(color string) {
c.color = color
}
func main() {
c := car{
color: "white",
}
c.setColor("blue")
fmt.Println(c.color)
// prints "blue"
}
Receptor não apontador
type car struct {
color string
}
func (c car) setColor(color string) {
c.color = color
}
func main() {
c := car{
color: "white",
}
c.setColor("blue")
fmt.Println(c.color)
// prints "white"
}
Métodos com receptores de ponteiro não exigem o uso de um ponteiro para chamá-los. O ponteiro será derivado automaticamente do valor.
type circle struct {
x int
y int
radius int
}
func (c *circle) grow(){
c.radius *= 2
}
func main(){
c := circle{
x: 1,
y: 2,
radius: 4,
}
// notice c is not a pointer in the calling function
// but the method still gains access to a pointer to c
c.grow()
fmt.Println(c.radius)
// prints 8
}
Capítulo 13 – Ambiente de Desenvolvimento Local em Go
Pacotes
Certifique-se de ter o Go instalado na sua máquina local.
Cada programa Go é composto de pacotes.
Você provavelmente já notou o package main
no topo de todos os programas que você escreveu.
Um pacote chamado "main" tem um ponto de entrada na main()
função. Um main
pacote é compilado em um programa executável.
Um pacote com qualquer outro nome é um "pacote de biblioteca". Bibliotecas não têm ponto de entrada. Bibliotecas simplesmente exportam funcionalidades que podem ser usadas por outros pacotes. Por exemplo:
package main
import (
"fmt"
"math/rand"
)
func main() {
fmt.Println("My favorite number is", rand.Intn(10))
}
Este programa é um executável. É um pacote "principal" e importa pacotes da biblioteca fmt
e .math/rand
Nomes dos pacotes
Convenção de Nomenclatura
Por convenção , o nome de um pacote é o mesmo que o último elemento do seu caminho de importação. Por exemplo, o math/rand
pacote inclui arquivos que começam com:
package rand
Dito isso, os nomes dos pacotes não precisam corresponder ao caminho de importação. Por exemplo, eu poderia escrever um novo pacote com o caminho github.com/mailio/rand
e nomeá-lo random
:
package random
Embora isso seja possível, não é recomendado por uma questão de consistência.
Um pacote / diretório
Um diretório de código Go pode ter no máximo um pacote. Todos .go
os arquivos em um único diretório devem pertencer ao mesmo pacote. Caso contrário, o compilador gerará um erro. Isso vale tanto para os pacotes principais quanto para os de biblioteca.
Módulos Go
Os programas Go são organizados em pacotes . Um pacote é um diretório de código Go compilado em conjunto. Funções, tipos, variáveis e constantes definidos em um arquivo-fonte são visíveis para todos os outros arquivos-fonte dentro do mesmo pacote (diretório) .
Um repositório contém um ou mais módulos . Um módulo é uma coleção de pacotes Go lançados juntos.
Um repositório Go normalmente contém apenas um módulo, localizado na raiz do repositório.
Um arquivo nomeado go.mod
na raiz de um projeto declara o módulo. Ele contém:
-
O caminho do módulo
-
A versão da linguagem Go que seu projeto requer
-
Opcionalmente, quaisquer dependências de pacotes externos que seu projeto tenha
O caminho do módulo é apenas o prefixo do caminho de importação para todos os pacotes dentro do módulo. Aqui está um exemplo de um go.mod
arquivo:
module github.com/bootdotdev/exampleproject
go 1.20
require github.com/google/examplepackage v1.3.0
O caminho de cada módulo não serve apenas como um prefixo de caminho de importação para os pacotes, mas também indica onde o comando go deve procurar para baixá-lo .
Por exemplo, para baixar o módulo golang.org/x/tools
, o comando go consultaria o repositório localizado em https://golang.org/x/tools .
Um "caminho de importação" é uma string usada para importar um pacote. O caminho de importação de um pacote é o caminho do módulo unido ao seu subdiretório dentro do módulo. Por exemplo, o módulo
github.com/google/go-cmp
contém um pacote no diretóriocmp/
. O caminho de importação desse pacote égithub.com/google/go-cmp/cmp
. Pacotes na biblioteca padrão não têm um prefixo de caminho de módulo. – Parafraseado da organização de código do Golang.org
Preciso colocar meu pacote no GitHub?
Você não precisa publicar seu código em um repositório remoto antes de poder compilá-lo. Um módulo pode ser definido localmente sem pertencer a um repositório. Mas é um bom hábito manter uma cópia de todos os seus projetos em um servidor remoto, como o GitHub.
Como configurar sua máquina
Sua máquina conterá muitos repositórios de controle de versão (gerenciados pelo Git, por exemplo).
Cada repositório contém um ou mais pacotes , mas normalmente será um único módulo .
Cada pacote consiste em um ou mais arquivos de origem Go em um único diretório.
O caminho para o diretório de um pacote determina seu caminho de importação e de onde ele pode ser baixado se você decidir hospedá-lo em um sistema de controle de versão remoto, como Github ou Gitlab.
Uma nota sobre GOPATH
A variável de ambiente $GOPATH será definida por padrão em algum lugar da sua máquina (normalmente no diretório home, ~/go
). Como trabalharemos na nova configuração "Módulos Go", você não precisa se preocupar com isso . Se você leu algo online sobre como configurar seu GOPATH, essa documentação provavelmente está desatualizada.
Hoje em dia, você deve evitar trabalhar no $GOPATH/src
diretório. Novamente, essa é a maneira antiga de fazer as coisas e pode causar problemas inesperados, então é melhor simplesmente evitá-la.
Entre no seu espaço de trabalho
Navegue até um local na sua máquina onde você deseja armazenar algum código. Por exemplo, eu armazeno todo o meu código em ~/workspace
, e depois o organizo em subpastas com base no local remoto. Por exemplo,
~/workspace/github.com/wagslane/go-password-validator
= https://github.com/wagslane/go-password-validator
Dito isto, você pode colocar seu código onde quiser.
Como escrever seu primeiro programa Go local
Uma vez dentro do seu espaço de trabalho pessoal, crie um novo diretório e entre nele:
mkdir hellogo
cd hellogo
Dentro do diretório, declare o nome do seu módulo:
go mod init {REMOTE}/{USERNAME}/hellogo
Onde {REMOTE}
está o seu provedor de origem remota preferido (ou seja, github.com
) e {USERNAME}
qual é o seu nome de usuário do Git. Se você ainda não usa um provedor remoto, basta usarexample.com/username/hellogo
Imprima seu go.mod
arquivo:
cat go.mod
O comando Go Run
Dentro hellogo
, crie um novo arquivo chamado main.go
.
Convencionalmente, o arquivo no main
pacote que contém a main()
função é chamado main.go
.
Cole o seguinte código no seu arquivo:
package main
import "fmt"
func main() {
fmt.Println("hello world")
}
Execute o código
go run main.go
O go run
comando é usado para compilar e executar rapidamente um pacote Go. O binário compilado não é salvo no seu diretório de trabalho. Use- go build
o para compilar executáveis de produção.
Raramente uso, go run
exceto para fazer algum teste ou depuração rapidamente.
Leitura adicional
Execute go help run
no seu shell e leia as instruções.
O comando Go Build
go build
compila o código em um programa executável.
Construir um executável
Certifique-se de que você está no seu repositório hellogo e execute:
go build
Execute o novo programa:
./hellogo
Vá instalar
Construir um executável
Certifique-se de que você está no seu hellogo
repositório e execute:
go install
Navegue para fora do diretório do seu projeto:
cd ../
Go instalou o hellogo
programa globalmente. Execute-o com:
hellogo
Dica sobre "não encontrado"
Se você receber um erro sobre "hellogo não encontrado", provavelmente seu ambiente Go não está configurado corretamente. Especificamente, go install
está adicionando seu binário ao seu GOBIN
diretório, mas ele pode não estar no seu PATH
.
Você pode ler mais sobre isso aqui na documentação do go install .
Como criar um pacote Go personalizado
Vamos escrever um pacote para importar e usar em hellogo
.
Crie um diretório irmão no mesmo nível do hellogo
diretório:
mkdir mystrings
cd mystrings
Inicializar um módulo:
go mod init {REMOTE}/{USERNAME}/mystrings
Em seguida, crie um novo arquivo mystrings.go
nesse diretório e cole o seguinte código:
// by convention, we name our package the same as the directory
package mystrings
// Reverse reverses a string left to right
// Notice that we need to capitalize the first letter of the function
// If we don't then we won't be able access this function outside of the
// mystrings package
func Reverse(s string) string {
result := ""
for _, v := range s {
result = string(v) + result
}
return result
}
Observe que não há main.go
ou func main()
neste pacote.
go build
não compilará um executável a partir de um pacote de biblioteca. No entanto, go build
ainda compilará o pacote e o salvará em nosso cache de compilação local. É útil para verificar erros de compilação.
Correr:
go build
Como publicar pacotes remotos em Go
Vamos aprender a usar um pacote de código aberto disponível online.
Uma nota sobre como você deve publicar módulos
Esteja ciente de que usar a palavra-chave "replace" como fizemos na última tarefa não é recomendado , mas pode ser útil para começar a trabalhar rapidamente. A maneira correta de criar e depender de módulos é publicá-los em um repositório remoto. Ao fazer isso, a palavra-chave "replace" pode ser removida do go.mod
:
Ruim
Isto funciona apenas para o desenvolvimento local
module github.com/wagslane/hellogo
go 1.20
replace github.com/wagslane/mystrings v0.0.0 => ../mystrings
require (
github.com/wagslane/mystrings v0.0.0
)
Bom
Isso funciona se publicarmos nossos módulos em um local remoto, como o Github, como deveríamos.
module github.com/wagslane/hellogo
go 1.20
require (
github.com/wagslane/mystrings v0.0.0
)
Melhores práticas com pacotes Go
Muitas vezes vi, e fui responsável por, colocar código em pacotes sem pensar muito. Rapidamente tracei um limite e comecei a colocar código em pastas diferentes (que em Go são pacotes diferentes por definição) apenas para facilitar a localização.
Aprender a criar corretamente pacotes pequenos e reutilizáveis pode levar sua carreira em Go para o próximo nível.
1. Ocultar lógica interna
Se você está familiarizado com os pilares da POO, esta é uma prática de encapsulamento .
Muitas vezes, um aplicativo terá uma lógica complexa que exige muito código. Em quase todos os casos, a lógica com a qual o aplicativo se importa pode ser exposta por meio de uma API, e a maior parte do trabalho pesado pode ser mantida em um pacote.
Por exemplo, imagine que estamos construindo uma aplicação que precisa classificar imagens. Poderíamos construir um pacote:
package classifier
// ClassifyImage classifies images as "hotdog" or "not hotdog"
func ClassifyImage(image []byte) (imageType string) {
return hasHotdogColors(image) && hasHotdogShape(image)
}
func hasHotdogShape(image []byte) bool {
// internal logic that the application doesn't need to know about
return true
}
func hasHotdogColors(image []byte) bool {
// internal logic that the application doesn't need to know about
return true
}
Criamos uma API expondo apenas a(s) função(ões) que o nível do aplicativo precisa conhecer. Toda a outra lógica não é exportada para manter uma separação clara de interesses. O aplicativo não precisa saber como classificar uma imagem, apenas o resultado da classificação.
2. Não altere as APIs
As funções não exportadas dentro de um pacote podem e devem mudar frequentemente para testes, refatoração e correção de bugs.
Uma biblioteca bem projetada terá uma API estável para que os usuários não recebam alterações drásticas sempre que atualizarem a versão do pacote. Em Go, isso significa não alterar as assinaturas das funções exportadas.
3. Não exporte funções do pacote principal
Um main
pacote não é uma biblioteca, não há necessidade de exportar funções dele.
4. Os pacotes não devem saber sobre dependentes
Talvez uma das regras mais importantes e quebradas seja a de que um pacote não deve saber nada sobre seus dependentes. Em outras palavras, um pacote nunca deve ter conhecimento específico sobre um aplicativo específico que o utiliza.
Leitura adicional
Opcionalmente, você pode ler mais aqui se estiver interessado.
Capítulo 14 – Canais em Go
Concorrência
Concorrência é a capacidade de executar várias tarefas simultaneamente. Normalmente, nosso código é executado uma linha de cada vez, uma após a outra. Isso é chamado de execução sequencial ou execução síncrona .
Se o computador em que estamos executando nosso código tiver vários núcleos, podemos até executar várias tarefas exatamente ao mesmo tempo. Se estivermos executando em um único núcleo, um único código executa código quase ao mesmo tempo, alternando entre tarefas muito rapidamente. De qualquer forma, o código que escrevemos parece o mesmo em Go e aproveita todos os recursos disponíveis.
Como funciona a simultaneidade em Go?
O Go foi projetado para ser concorrente, o que é uma característica bastante exclusiva dele. Ele se destaca na execução de muitas tarefas simultaneamente e com segurança, usando uma sintaxe simples.
Não existe uma linguagem de programação popular em que a geração de execução simultânea seja tão elegante, pelo menos na minha opinião.
A simultaneidade é tão simples quanto usar a go
palavra-chave ao chamar uma função:
go doSomething()
No exemplo acima, doSomething()
será executado simultaneamente com o restante do código na função. A go
palavra-chave é usada para gerar uma nova goroutine .
Canais em Go
Canais são uma fila tipada e thread-safe. Canais permitem que diferentes goroutines se comuniquem entre si.
Criar um canal
Assim como mapas e fatias, os canais devem ser criados antes do uso. Eles também usam a mesma make
palavra-chave:
ch := make(chan int)
Enviar dados para um canal
ch <- 69
O <-
operador é chamado de operador de canal . Os dados fluem na direção da seta. Esta operação será bloqueada até que outra goroutine esteja pronta para receber o valor.
Receber dados de um canal
v := <-ch
Isto lê e remove um valor do canal e o salva na variável v
. Esta operação será bloqueada até que haja um valor no canal para ser lido.
Bloqueios e deadlocks
Um deadlock ocorre quando um grupo de goroutines está bloqueando todas, impedindo que qualquer uma delas continue. Este é um bug comum ao qual você precisa ficar atento em programação concorrente.
Fichas
Estruturas vazias são frequentemente usadas, como tokens
em programas em Go. Nesse contexto, um token é um valor unário . Em outras palavras, não nos importamos com o que é passado pelo canal. Nos importamos com quando e se é passado.
Podemos bloquear e esperar até que algo seja enviado em um canal usando a seguinte sintaxe
<-ch
Isso bloqueará até que um único item seja retirado do canal e, então, continuará descartando o item.
Canais com buffer
Os canais podem ser opcionalmente armazenados em buffer.
Como criar um canal com buffer
Você pode fornecer um comprimento de buffer como o segundo argumento para make()
criar um canal em buffer:
ch := make(chan int, 100)
O envio em um canal com buffer só é bloqueado quando o buffer está cheio .
Recebendo blocos somente quando o buffer estiver vazio .
Como Fechar Canais
Os canais podem ser fechados explicitamente por um remetente :
ch := make(chan int)
// do some stuff with the channel
close(ch)
Como verificar se um canal está fechado
Semelhante ao ok
valor ao acessar dados em um map
, os receptores podem verificar o ok
valor ao receber de um canal para testar se um canal foi fechado.
v, ok := <-ch
ok é false
se o canal estiver vazio e fechado.
Não envie em um canal fechado
Enviar em um canal fechado causará pânico. Um pânico na goroutine principal fará com que todo o programa trave, e um pânico em qualquer outra goroutine fará com que essa goroutine trave.
Não é necessário fechar. Não há problema em deixar canais abertos; eles continuarão sendo coletados como lixo se não forem utilizados. Você deve fechar os canais para indicar explicitamente ao receptor que nada mais será transmitido.
Alcance sobre um canal
Semelhante a fatias e mapas, os canais podem ser classificados.
for item := range ch {
// item is the next value received from the channel
}
Este exemplo receberá valores no canal (bloqueando a cada iteração se não houver nada de novo) e sairá somente quando o canal for fechado.
Selecione um canal
Às vezes, temos uma única goroutine ouvindo vários canais e queremos processar os dados na ordem em que eles chegam em cada canal.
Uma select
instrução é usada para ouvir vários canais ao mesmo tempo. É semelhante a uma switch
instrução, mas para canais.
select {
case i, ok := <- chInts:
fmt.Println(i)
case s, ok := <- chStrings:
fmt.Println(s)
}
O primeiro canal com um valor pronto para ser recebido será disparado e seu corpo será executado. Se vários canais estiverem prontos ao mesmo tempo, um será escolhido aleatoriamente. A ok
variável no exemplo acima indica se o canal já foi fechado pelo remetente.
Selecione Padrão
O default
caso em uma select
instrução é executado imediatamente se nenhum outro canal tiver um valor pronto. Um default
caso impede o select
bloqueio da instrução.
select {
case v := <-ch:
// use v
default:
// receiving from ch would block
// so do something else
}
Capítulo 15 – Mutexes em Go
Mutexes nos permitem bloquear o acesso aos dados. Isso garante que possamos controlar quais goroutines podem acessar determinados dados em cada momento.
A biblioteca padrão do Go fornece uma implementação integrada de um mutex com o tipo sync.Mutex e seus dois métodos:
Podemos proteger um bloco de código envolvendo-o com uma chamada para Lock
and , Unlock
como mostrado no protected()
método abaixo.
É uma boa prática estruturar o código protegido dentro de uma função para que isso defer
possa ser usado para garantir que nunca nos esqueçamos de desbloquear o mutex.
func protected(){
mux.Lock()
defer mux.Unlock()
// the rest of the function is protected
// any other calls to `mux.Lock()` will block
}
Mutexes são poderosos. Como a maioria das coisas poderosas, eles também podem causar muitos bugs se usados de forma descuidada.
Os mapas não são seguros para threads
Mapas não são seguros para uso simultâneo! Se você tiver várias goroutines acessando o mesmo mapa e pelo menos uma delas estiver gravando no mapa, você deve bloquear seus mapas com um mutex.
Por que é chamado de Mutex?
Mutex é a abreviação de exclusão mútua , e o nome convencional para a estrutura de dados que a fornece é "mutex", geralmente abreviado para "mux".
É chamado de "exclusão mútua" porque um mutex impede que diferentes threads (ou goroutines) acessem os mesmos dados ao mesmo tempo.
Por que usar mutexes?
O principal problema que os mutexes nos ajudam a evitar é o problema de leitura/escrita simultânea . Esse problema surge quando uma thread está gravando em uma variável enquanto outra thread está lendo dessa mesma variável ao mesmo tempo .
Quando isso acontece, um programa Go entra em pânico porque o leitor pode estar lendo dados ruins enquanto eles estão sendo mutados no local.
Exemplo de mutex
package main
import (
"fmt"
)
func main() {
m := map[int]int{}
go writeLoop(m)
go readLoop(m)
// stop program from exiting, must be killed
block := make(chan struct{})
<-block
}
func writeLoop(m map[int]int) {
for {
for i := 0; i < 100; i++ {
m[i] = i
}
}
}
func readLoop(m map[int]int) {
for {
for k, v := range m {
fmt.Println(k, "-", v)
}
}
}
O exemplo acima cria um mapa e, em seguida, inicia duas goroutines, cada uma com acesso ao mapa. Uma goroutine altera continuamente os valores armazenados no mapa, enquanto a outra imprime os valores que encontra no mapa.
Se executarmos o programa em uma máquina multi-core, obteremos a seguinte saída:fatal error: concurrent map iteration and map write
Em Go, não é seguro ler e escrever em um mapa ao mesmo tempo.
Mutexes para o resgate
package main
import (
"fmt"
"sync"
)
func main() {
m := map[int]int{}
mux := &sync.Mutex{}
go writeLoop(m, mux)
go readLoop(m, mux)
// stop program from exiting, must be killed
block := make(chan struct{})
<-block
}
func writeLoop(m map[int]int, mux *sync.Mutex) {
for {
for i := 0; i < 100; i++ {
mux.Lock()
m[i] = i
mux.Unlock()
}
}
}
func readLoop(m map[int]int, mux *sync.Mutex) {
for {
mux.Lock()
for k, v := range m {
fmt.Println(k, "-", v)
}
mux.Unlock()
}
}
Neste exemplo, adicionamos um sync.Mutex{}
e o nomeamos mux
. No loop de escrita, o Lock()
método é chamado antes da escrita e, em seguida, o Unlock()
é chamado quando terminamos. Essa sequência de Bloqueio/Desbloqueio garante que nenhuma outra thread possa acessar Lock()
o mutex enquanto ele estiver bloqueado – qualquer outra thread que tentar Lock()
bloqueará e esperará até que executemos Unlock()
.
No leitor, Lock()
antes de iterar sobre o mapa e também Unlock()
quando terminamos. Agora as threads compartilham a memória com segurança!
RWMutex
A biblioteca padrão também expõe um sync.RWMutex
Além destes métodos:
O sync.RWMutex
também possui estes métodos:
Isso sync.RWMutex
pode ajudar no desempenho se tivermos um processo de leitura intensiva. Muitas goroutines podem ler o mapa com segurança ao mesmo tempo (várias Rlock()
chamadas podem ocorrer simultaneamente). No entanto, apenas uma goroutine pode conter um Lock()
e todos RLock()
os 's também serão excluídos.
Capítulo 16 – Genéricos em Go
Como mencionamos, Go não suporta classes. Por muito tempo, isso significou que o código Go não podia ser facilmente reutilizado em muitas circunstâncias.
Por exemplo, imagine um código que divide uma fatia em duas partes iguais. O código que divide a fatia não se importa com os valores armazenados nela. Infelizmente, em Go, precisaríamos escrevê-lo várias vezes para cada tipo, o que é algo muito pouco árido de se fazer.
func splitIntSlice(s []int) ([]int, []int) {
mid := len(s)/2
return s[:mid], s[mid:]
}
func splitStringSlice(s []string) ([]string, []string) {
mid := len(s)/2
return s[:mid], s[mid:]
}
No entanto, no Go 1.20, o suporte para genéricos foi lançado, resolvendo efetivamente esse problema!
Parâmetros de tipo
Simplificando, os genéricos nos permitem usar variáveis para nos referir a tipos específicos. Esse é um recurso incrível, pois nos permite escrever funções abstratas que reduzem drasticamente a duplicação de código.
func splitAnySlice[T any](s []T) ([]T, []T) {
mid := len(s)/2
return s[:mid], s[mid:]
}
No exemplo acima, T
é o nome do parâmetro de tipo da splitAnySlice
função, e dissemos que ele deve corresponder à any
restrição, o que significa que pode ser qualquer coisa. Isso faz sentido porque o corpo da função não se importa com os tipos de coisas armazenadas na fatia.
firstInts, secondInts := splitAnySlice([]int{0, 1, 2, 3})
fmt.Println(firstInts, secondInts)
Por que genéricos?
Genéricos reduzem código repetitivo
Você deve se preocupar com genéricos porque eles significam que você não precisa escrever tanto código! Pode ser frustrante escrever a mesma lógica repetidamente, só porque você tem alguns tipos de dados subjacentes ligeiramente diferentes.
Os genéricos são usados com mais frequência em bibliotecas e pacotes
Os genéricos oferecem aos desenvolvedores de Go uma maneira elegante de escrever pacotes utilitários incríveis. Embora você veja e use genéricos no código de aplicativos, acredito que será muito mais comum vê-los usados em bibliotecas e pacotes. Bibliotecas e pacotes contêm código importável destinado a ser usado em diversos aplicativos, então faz sentido escrevê-los de uma forma mais abstrata. Os genéricos costumam ser a maneira de fazer exatamente isso!
Por que demorou tanto para chegar os genéricos?
Go enfatiza a simplicidade. Em outras palavras, Go propositalmente deixou de lado muitos recursos para oferecer seu melhor: ser simples e fácil de usar.
De acordo com dados históricos de pesquisas sobre Go , a falta de genéricos em Go sempre foi listada como um dos três maiores problemas da linguagem. Em certo ponto, as desvantagens associadas à falta de um recurso como genéricos justificam a adição de complexidade à linguagem.
Restrições em Go
Às vezes, você precisa que a lógica da sua função genérica saiba algo sobre os tipos com os quais ela opera. O exemplo que usamos no primeiro exercício não precisava saber nada sobre os tipos no slice, então usamos a restrição interna any
:
func splitAnySlice[T any](s []T) ([]T, []T) {
mid := len(s)/2
return s[:mid], s[mid:]
}
Restrições são apenas interfaces que nos permitem escrever genéricos que operam apenas dentro da restrição de um determinado tipo de interface. No exemplo acima, a any
restrição é a mesma da interface vazia, pois significa que o tipo em questão pode ser qualquer coisa .
Como criar uma restrição personalizada
Vamos dar uma olhada no exemplo de uma concat
função. Ela recebe uma fatia de valores e os concatena em uma string. Isso deve funcionar com qualquer tipo que possa se representar como uma string , mesmo que não seja uma string por baixo dos panos.
Por exemplo, uma user
struct pode ter uma .String()
que retorna uma string com o nome e a idade do usuário.
type stringer interface {
String() string
}
func concat[T stringer](vals []T) string {
result := ""
for _, val := range vals {
// this is where the .String() method
// is used. That's why we need a more specific
// constraint instead of the any constraint
result += val.String()
}
return result
}
Listas de tipos de interface
Quando os genéricos foram lançados, uma nova maneira de escrever interfaces também foi lançada ao mesmo tempo!
Agora podemos simplesmente listar vários tipos para obter uma nova interface/restrição.
// Ordered is a type constraint that matches any ordered type.
// An ordered type is one that supports the <, <=, >, and >= operators.
type Ordered interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
~float32 | ~float64 |
~string
}
Como nomear tipos genéricos
Vejamos novamente este exemplo simples:
func splitAnySlice[T any](s []T) ([]T, []T) {
mid := len(s)/2
return s[:mid], s[mid:]
}
Lembre-se, T
é apenas um nome de variável. Poderíamos ter nomeado o parâmetro de tipo com qualquer nome . T
Acontece que é uma convenção bastante comum para uma variável de tipo, semelhante a como i
é uma convenção para variáveis de índice em loops.
Isto é igualmente válido:
func splitAnySlice[MyAnyType any](s []MyAnyType) ([]MyAnyType, []MyAnyType) {
mid := len(s)/2
return s[:mid], s[mid:]
}