Golang – 03. Structs e Funções e Métodos
- Golang – 01. Introdução
- Golang – 02. Tipos Básicos
- Golang – 03. Structs e Funções e Métodos
- Golang – 04. Estruturas de Controle
- Golang – 05. Gerenciando Pacotes
- Golang – 06. Biblioteca Padrão I – fmt e strings
- Golang – 07. Biblioteca Padrão II – os, os/exec e os/user
- Golang – 08. Interface
- Golang – 09. Goroutines – Concorrência e Paralelismo
- Golang – 10. Goroutines – Canais
- Golang – 11. Pacotes e Documentação no Go
Índice
- 1. Structs
- 1.1. Composição
- 2. Funções
- 2.1. Retorno Simples
- 2.2. Retorno Nomeado
- 2.3. Vários Retornos: aprimorando segGrau
- 2.4. Funções como valor e ignorar retorno, “_”
- 2.5. Funções Anônimas: Uso Geral e Flexibilidade
- 2.5.1. Anônima: Recebendo Argumento
- 2.5.2. Anônima: Retornando um valor
- 2.6. Funções Variádicas
- 2.7. Função init()
- 2.8. Definindo um Método
- 2.9. Alterando Valor do Argumento
- 3. Considerações Finais
Explorando a última estrutura de dados fundamental do Go, este capítulo se concentra nas structs, que são uma maneira flexível e poderosa de organizar os dados no código. Em Go, uma struct pode ser considerada o mais próximo de uma classe em outras linguagens orientadas a objetos. Além disso, abordaremos as funções e os métodos em Go, destacando como esses elementos se integram com as structs para criar um design de código eficiente e modular.
As structs em Go são usadas para agrupar variáveis de diferentes tipos sob um nome único, fornecendo uma forma de representar dados complexos. Além de explorar a definição e utilização das structs, discutiremos também como Go permite associar funções a structs, conhecidas como métodos, dando um aspecto mais orientado a objetos à linguagem.
Dentro do escopo de funções, exploraremos diversos conceitos importantes, incluindo funções anônimas, funções variádicas e a função especial init()
. As funções anônimas em Go oferecem uma abordagem flexível para a execução de código sem a necessidade de nomeá-lo, enquanto as funções variádicas permitem argumentos de número variável, aumentando a flexibilidade do código. A função init()
, por outro lado, desempenha um papel crucial na inicialização de variáveis ou configurações antes do início da execução principal do programa.
Neste capítulo também veremos como modificar os valores dos argumentos em funções e a importância da passagem de argumentos por valor ou por referência, ilustrado por meio da função swap
. Estes conceitos são fundamentais para entender a manipulação de dados e o comportamento das funções em Go.
Ao final deste capítulo, você terá uma compreensão abrangente de como as structs funcionam em Go, como definir e utilizar métodos, e como diferentes tipos de funções podem ser aplicadas para tornar seu código mais eficiente e organizado.
Structs
Em Go, uma struct
é um tipo de dados composto que permite agrupar variáveis (chamadas de campos) de diferentes tipos sob um único nome. As structs
são semelhantes a classes em algumas linguagens orientadas a objetos, como Java ou C++, mas existem diferenças significativas:
- Métodos e Encapsulamento:
- Em linguagens orientadas a objetos, uma classe não só define campos, mas também métodos (funções) associados a esses campos. Em Go, os métodos não são definidos dentro da
struct
, mas são associados a ela por meio de uma declaração de método separada. - O encapsulamento, um conceito chave em orientação a objetos, é obtido em Go através da exportação de campos (iniciando o nome do campo com uma letra maiúscula para torná-lo público) ou mantendo-os não exportados (iniciando com uma letra minúscula).
- Em linguagens orientadas a objetos, uma classe não só define campos, mas também métodos (funções) associados a esses campos. Em Go, os métodos não são definidos dentro da
- Herança e Polimorfismo:
- As classes em linguagens orientadas a objetos muitas vezes suportam herança e polimorfismo. Go não suporta herança no sentido tradicional. Em vez disso, a reutilização e o compartilhamento de comportamento são alcançados por meio de composição (incorporando uma
struct
dentro de outra) e interfaces. - O polimorfismo em Go é alcançado principalmente através do uso de interfaces, que permitem que diferentes tipos (incluindo
structs
) sejam tratados de maneira uniforme se eles implementarem o mesmo conjunto de métodos.
- As classes em linguagens orientadas a objetos muitas vezes suportam herança e polimorfismo. Go não suporta herança no sentido tradicional. Em vez disso, a reutilização e o compartilhamento de comportamento são alcançados por meio de composição (incorporando uma
- Construtores e Destrutores:
- Em linguagens orientadas a objetos, classes geralmente têm construtores e destrutores para inicialização e limpeza. Em Go, a inicialização é geralmente feita através de funções que retornam uma instância do tipo
struct
, e não há destrutores. A limpeza, quando necessária, é gerenciada manualmente ou por meio de deferência e o garbage collector do Go.
- Em linguagens orientadas a objetos, classes geralmente têm construtores e destrutores para inicialização e limpeza. Em Go, a inicialização é geralmente feita através de funções que retornam uma instância do tipo
- Estado e Comportamento:
- Uma classe tradicionalmente encapsula tanto estado (dados) quanto comportamento (métodos). Em Go, embora você possa associar métodos a uma
struct
, a definição dastruct
em si contém apenas o estado.
- Uma classe tradicionalmente encapsula tanto estado (dados) quanto comportamento (métodos). Em Go, embora você possa associar métodos a uma
Em resumo, enquanto structs
em Go e classes em linguagens orientadas a objetos têm o propósito comum de agrupar dados relacionados, elas funcionam de maneira diferente. Go adota uma abordagem mais minimalista e modular, enfatizando a composição sobre a herança e o uso de interfaces para polimorfismo.
Uma struct
é uma coleção de campos. Cada campo tem um nome e um tipo:
type detalhes struct {
segmento string
marca string
tipo string
}
Esta struct em Go pode ser definida e preenchida de quatro formas:
- diretamente com o operador de declaração curta “
:=
“:
dcComics := detalhes{segmento: "Comics", marca: "DC", tipo: "Livros"}
Esta forma é útil para inicializações curtas e diretas, especialmente quando a clareza do código é uma prioridade.
- sem os nomes dos campos:
dcComics := detalhes{"Comics", "DC", "Livros"}
Embora esta abordagem seja mais concisa, ela requer que você mantenha a ordem dos campos conforme definido na struct, o que pode ser propenso a erros se a struct for modificada posteriormente.
- em diversas linhas
dcComics := detalhes{
segmento: "Comics",
marca: "DC",
tipo: "Livros",
}
Essa abordagem é mais legível, especialmente para structs com muitos campos, facilitando a manutenção e compreensão do código.
- ou através de declaração com o
var
e preenchendo os campos como se acessasse um campo de um objeto:
var dcComics detalhes
dcComics.segmento = "Comics",
dcComics.marca = "DC",
dcComics.tipo = "Livros",
Esta técnica é particularmente útil quando a inicialização dos campos precisa ser feita em diferentes partes do código, oferecendo flexibilidade na atribuição de valores.
A semelhança com a instância de uma classe fica ainda mais evidente com a forma como os campos da struct dcComics
são acessados: dcComics.segmento
, dcComics.marca
e dcComics.tipo
. Isso ressalta como a funcionalidade de acesso aos campos em uma struct
em Go é similar ao acesso de atributos em uma classe em outras linguagens de programação orientadas a objetos.
É importante destacar que, em Go, as structs são valores e não referências. Isso significa que, ao passar uma struct para uma função, você está passando uma cópia dela. Para modificar a struct original, você deve passar um ponteiro para ela. Essa característica é crucial para entender como gerenciar o estado e a imutabilidade em Go.
Composição
O Go não possui herança, mas sim uma composição, que traz a ideia de herança, porém sem sua plenitude. A visão dos desenvolvedores do Go é de que a “herança é uma forma de quebrar o encapsulamento”, uma habilidade da linguagem que é prioritária em seu desenvolvimento.
A composição em Go funciona como uma pseudo-herança em structs, que pode ser implementada passando o nome da estrutura como parte dos “campos” da nova struct. Por exemplo, a struct revista
tem os campos da struct detalhes
e pode ser declarada como:
type revista struct {
nome string
autores string
preco float32
detalhes
}
A struct detalhes
faz parte de seus argumentos, mas sem um nome de campo à sua esquerda. O código a seguir implementa as structs detalhes
e revista
, acima:
package main import "fmt" type detalhes struct { segmento string marca string tipo string } type revista struct { nome string autores string preco float32 detalhes } func main() { dcComics := detalhes{"Comics", "DC", "Livros"} revista0 := revista{ nome: "Batman: Ano Um", autores: "David Mazzucchelli, Frank Miller", preco: 66.90, detalhes: dcComics, } revista1 := revista{"Watchmen", "Alan Moore, Dave Gibbons", 184.90, dcComics} fmt.Println(revista0) fmt.Println(revista1) fmt.Println(revista0.segmento) }
A saída deste código é apresentada a seguir:
{Batman: Ano Um David Mazzucchelli, Frank Miller 66.9 {Comics DC Livros}}
{Watchmen Alan Moore, Dave Gibbons 184.9 {Comics DC Livros}}
Comics
Observe que na linha 32 o campo segmento
é herdado da struct detalhes
, como se estivesse definido dentro da struct revista
.
Uma forma clara de perceber que não se trata bem de uma herança é o fato de um novo produto
que não tenha os mesmos campos da struct dcComics
poderia ser declarado em texto corrido como:
... revista2 := revista{ nome: "Shazam!", autores: "Clayton Henry, Tim Sheridan", preco: 24.90, detalhes: detalhes{ segmento: "Comics", marca: "DC", tipo: "Revista", }, } fmt.Println(revista2) }
Adicione o código acima a partir da linha 32 para acrescentar uma nova revista. Esta revista2
adiciona a revista “Shazam!”, com detalhes diferentes ao dcComics
, recebido em revista0
e revista1
, no campo tipo: "Revista"
.
Se comentar as linhas 7 e 11, no código acima, o código restante ficaria mais coerente ao acesso a campos herdados. No entanto, isso geraria um erro de tipo não reconhecido, já que os campos segmento
, marca
e tipo
não são definidos na struct revista
.
revista2 := revista{ nome: "Shazam!", autores: "Clayton Henry, Tim Sheridan", preco: 24.90, // detalhes: detalhes{ segmento: "Comics", marca: "DC", tipo: "Revista", // }, } fmt.Println(revista2) }
Entretanto, outras características permanecem, como consultas dos conteúdos revista2.segmento
, revista2.marca
ou revista2.tipo
funcionam como se fossem herdados da struct detalhes
. Portanto, mesmo que uma composição tenha sim uma certa semelhança com herança, ela se diferencia por promover um design mais modular e desacoplado, características essenciais para a manutenção e escalabilidade do código em Go.
Funções
Funções em Go são similares a outras linguagens de programação como C, Python, Java, etc. Ao longo desta série, a função main
tem sido empregada a cada novo código. Agora, vamos estender esta definição para outras funções.
A sintaxe de uma função em Go é a seguinte:
func nomeDaFunçao(arg1, arg2, ...) (ret1, ret2, ...) { corpo da função }
Tanto os argumentos (arg1, arg2, ...
) quanto os retornos (retorno1, retorno2, ...
) são opcionais. Se não houver retorno ou apenas um retorno, os parênteses podem ser omitidos.
Retorno Simples
O código a seguir define uma função para determinar a área de um quadrado:
func areaQuad(aresta int) int { return lado * lado }
areaQuad
é o nome da função, e lado
é seu argumento de entrada. O retorno é um inteiro representando a área do quadrado, calculada pela expressão lado * lado
.
Retorno Nomeado
Uma segunda forma de retorno em Go é declarar a variável de retorno no cabeçalho da função. Isso é conhecido como retorno nomeado. Veja o exemplo com a função areaRet
para calcular a área de um retângulo:
func areaRet(base, largura int) (area int) { area = base * largura return }
Neste caso, a variável area
é declarada no cabeçalho da função ((area int)
), representando o valor de retorno. Isso elimina a necessidade de especificar explicitamente qual variável está sendo retornada. A função simplesmente utiliza a palavra-chave return
para retornar o valor atual da variável area
.
O código a seguir reuni as duas funções, com retorno nomeado:
package main import "fmt" // areaQuad calcula a área de um quadrado dado o tamanho de sua aresta. // Utiliza retorno nomeado para maior clareza. func areaQuad(aresta int) (area int) { area = aresta * aresta // Calcula a área do quadrado return // Retorna o valor da área } // areaRet calcula a área de um retângulo dados base e altura. // Também utiliza retorno nomeado para consistência e clareza. func areaRet(base, largura int) (area int) { area = base * largura // Calcula a área do retângulo return // Retorna o valor da área } func main() { a, b := 5, 7 // Define valores para os lados do quadrado e do retângulo // Imprime a área de um quadrado com lado de tamanho 'a' fmt.Printf("Área de quadrado de aresta %d: %d\n", a, areaQuad(a)) // Imprime a área de um retângulo com base 'a' e altura 'b' fmt.Printf("Área de retângulo de %dx%d: %d\n", a, b, areaRet(a, b)) }
A saída deste código será:
alves@arabel:~$ go run func2.go
Área de quadrado de aresta 5: 25
Área de retângulo de 5x7: 35
Vários Retornos: aprimorando segGrau
No código anterior sobre a resolução de raízes de uma equação de segundo grau, o programa apresentava um erro ao tentar encontrar as raízes quando estas não existiam. Nesta seção, faremos duas melhorias:
- Utilização da função
segGrau
para encontrar as raízes. - Tratamento de erro mais apropriado, gerando uma mensagem de erro quando não houver raízes reais para o polinômio.
package main import ( "errors" "fmt" "log" "math" ) // segGrau calcula as raízes de uma equação do segundo grau. // Retorna um erro se as raízes não forem reais. func segGrau(a, b, c float64) (x1, x2 float64, err error) { delta := math.Pow(b, 2) - 4*a*c a2 := 2. * a if delta >= 0 { x1 = (-b + math.Sqrt(delta)) / a2 x2 = (-b - math.Sqrt(delta)) / a2 return } err = errors.New("raiz de número negativo") return } func main() { coef := [3][3]int{{2, 5, 3}, {2, -5, 3}, {5, 2, 3}} for _, c := range coef { fmt.Printf("%dx²+%dx+%d = 0\n", c[0], c[1], c[2]) x1, x2, err := segGrau(float64(c[0]), float64(c[1]), float64(c[2])) if err != nil { log.Fatal(err) } fmt.Printf(" => x1:%.2f x2: %.2f\n\n", x1, x2) } }
A saída deste código é:
alves@arabel:func02$ go run func.go
2x²+5x+3 = 0 => x1:-1.00 x2: -1.50
2x²+-5x+3 = 0 => x1:1.50 x2: 1.00
5x²+2x+3 = 0
2022/04/20 13:03:03 raiz de número negativo
exit status 1
Na função segGrau
, foram implementadas mudanças significativas para incorporar um tratamento de erro adequado. No cabeçalho da função, a variável err
é adicionada como um tipo error
, que é um tipo incorporado em Go destinado especificamente para o tratamento de erros. Se err
não for modificado ao longo da execução da função, seu valor será nil
, indicando a ausência de erros.
A lógica dentro da função segGrau
é estruturada para garantir a precisão e a segurança do cálculo das raízes. O if
na linha 15 verifica se a variável delta
é positiva, o que é uma condição necessária para a existência de raízes reais em uma equação do segundo grau. Se delta
for negativo, indicando a ausência de raízes reais, um novo erro é criado na linha 20 e atribuído a err
. Neste caso, a função retorna os valores padrão para x1
e x2
(0.0 e 0.0) junto com o erro.
Para testar a função com diferentes conjuntos de coeficientes, o código no método main
cria uma matriz de coeficientes na linha 25. O for
na linha 27 itera sobre esses conjuntos de coeficientes, passando cada um deles para a função segGrau
. A estrutura (_, c)
na instrução range
é usada para ignorar o índice do loop, concentrando-se apenas nos valores dos coeficientes.
Um ponto crítico no método main
é a verificação de erros na linha 30. Se err
indicar a presença de um erro, o pacote log
é usado para gerar um erro fatal com a função log.Fatal()
. Isso não só imprime a mensagem de erro no terminal, mas também interrompe a execução do programa. Se não houver erro, as raízes calculadas são impressas na linha 33.
Funções como valor e ignorar retorno, “_”
Em Go, assim como em outras linguagens de programação, funções são tratadas como valores de primeira classe. Isso significa que elas podem ser atribuídas a variáveis, passadas como argumentos para outras funções e retornadas por outras funções.
O código a seguir ilustra essa ideia ao transformar a função segGrau
em um valor de função atribuído à variável a
:
... func main() { a := segGrau fmt.Printf("Tipo de a: %T\n", a) x1, x2, _ := a(2, 5, 3) fmt.Println(x1, x2) }
Neste exemplo, a variável a
assume a função segGrau
. O tipo de a
é impresso, mostrando que é uma função com a assinatura func(float64, float64, float64) (float64, float64, error)
. Ao chamar a(2, 5, 3)
, ignoramos o valor de retorno de erro usando o operador “_
“. Isso é útil quando o valor de retorno não é necessário.
O próximo código mostra como criar “alias” para funções padrão de Go:
package main import ( "errors" "fmt" "math" ) var ( Pow = math.Pow Sqrt = math.Sqrt Print = fmt.Println ) func segGrau(a, b, c float64) (x1, x2 float64, err error) { delta := Pow(b, 2) - 4*a*c a2 := 2. * a if delta >= 0 { x1 = (-b + Sqrt(delta)) / a2 x2 = (-b - Sqrt(delta)) / a2 return } err = errors.New("raiz de número negativo") return } func main() { a := segGrau fmt.Printf("Tipo de a: %T\n", a) x1, x2, _ := a(2, 5, 3) Print(x1, x2) }
Neste código, as funções matemáticas math.Pow
e math.Sqrt
, assim como fmt.Println
, são renomeadas para Pow
, Sqrt
e Print
, respectivamente. Essa abordagem pode ser útil para simplificar o código onde essas funções são usadas frequentemente, tornando-o mais conciso e legível.
No entanto, é importante exercer cautela ao usar “alias” para funções padrão, pois isso pode tornar o código menos acessível para outros programadores que estão mais familiarizados com os nomes padrão das funções. Embora possa parecer uma maneira de limpar o código, pode, na realidade, introduzir confusão e dificultar a manutenção por outros desenvolvedores.
Funções Anônimas: Uso Geral e Flexibilidade
Funções anônimas, também conhecidas como funções lambda ou closures, são funções que são declaradas dentro de outra função (função base) e que não têm um nome associado. Em Go, elas são declaradas com a palavra-chave func
, seguida pelos seus argumentos de entrada, tipos de retorno e o corpo da função:
func(arg1 tipo1, arg2 tipo2, …) (ret1 tipoRetorno1, ret2 tipoRetorno2, …) {
// corpo da função
}
As funções anônimas são particularmente úteis quando um determinado conjunto de instruções precisa ser executado repetidamente dentro de uma função base, e não tem relevância ou utilidade fora desse contexto específico.
Um exemplo simples de uma função anônima pode ser visto no seguinte trecho de código “Hello World”, onde a função anônima é definida e invocada dentro da função main
:
package main import "fmt" func main() { fmt.Println() var hello = func() { fmt.Print("Hello World!!!") } hello() }
Neste exemplo, a função anônima é atribuída à variável hello
nas linhas 8 a 10. Ela é então invocada na linha 12, resultando na impressão de “Hello World!!!” no console.
Funções anônimas em Go são uma ferramenta poderosa que oferece flexibilidade e adaptabilidade em muitos contextos de programação. Elas se destacam por sua capacidade de serem definidas e usadas no local exato onde são necessárias, o que traz várias vantagens em um contexto mais amplo:
- Encapsulamento e Contextualização: Ao permitir a definição de lógicas e operações dentro do contexto em que são usadas, as funções anônimas aumentam a coesão e mantêm o escopo limitado, facilitando o entendimento e a manutenção do código.
- Uso em Callbacks e Iteradores: Elas são particularmente úteis em cenários como callbacks em manipuladores de eventos ou em funções de iteração, onde permitem a definição de comportamentos específicos e personalizados.
- Aplicação em Funções de Alta Ordem: Em funções que recebem outras funções como argumentos ou as retornam, as funções anônimas são uma escolha ideal para criar operações dinâmicas e específicas, como em funções de mapeamento ou redução.
- Simplicidade e Concisão: Para operações simples ou de uso único, uma função anônima pode ser uma solução mais direta e menos verbosa do que criar uma função nomeada separada.
- Facilidade de Modificação e Teste: Devido à proximidade com o seu contexto de uso, as funções anônimas são mais fáceis de modificar e testar, podendo acelerar o desenvolvimento em certas funcionalidades.
Contudo, é essencial manter um equilíbrio. Em situações onde a lógica é complexa ou é reutilizada em vários lugares, funções nomeadas podem ser mais adequadas. Além disso, para fins de depuração, as funções nomeadas são geralmente mais fáceis de rastrear em pilhas de chamadas.
Anônima: Recebendo Argumento
Funções anônimas em Go podem ser criadas e invocadas imediatamente, sem a necessidade de serem atribuídas a uma variável. Este padrão é útil para operações de curta duração e específicas do contexto em que são usadas. Um exemplo disso é uma reimplementação simplificada de uma saudação que recebe um argumento:
package main import "fmt" func main() { nome := "Roberta" func(n string) { fmt.Println("Hello " + n) }(nome) }
Neste exemplo, a função anônima é definida e executada dentro de main
, capturando e utilizando a variável nome
do seu escopo circundante. Esta abordagem é ideal para funcionalidades que não são reutilizadas fora do contexto atual, como pequenas operações de callback ou manipulação de eventos.
As funções anônimas podem ser particularmente úteis em cenários de iteração. Por exemplo, ao processar uma coleção de itens, uma função anônima pode ser usada para aplicar uma operação específica a cada item:
... for _, item := range items { func(i ItemType) { // processa 'i' }(item) } ...
Este padrão oferece flexibilidade e concisão, mas é importante considerar as implicações de performance. Funções anônimas que capturam muitas variáveis do escopo externo ou são usadas em loops intensivos podem afetar a performance. Nesses casos, uma função nomeada, especialmente se reutilizada em vários locais, pode ser uma escolha mais eficiente.
Anônima: Retornando um valor
Está função anônima não recebe ou retorna valores. O código seguinte implementa a função fatorial como uma função anônima que recebe um argumento, n
, e retorna o fatorial do argumento.
package main import "fmt" func main() { fat := func(n int) int { f := 1 for i := n; i > 1; i-- { f *= i } return f } fmt.Println(fat(3)) // 6 fmt.Println(fat(4)) // 24 fmt.Println(fat(5)) // 120 fmt.Println(fat(6)) // 720 }
Este exemplo mostra como uma função anônima pode ser utilizada para calcular o fatorial de um número, demonstrando sua utilidade em operações matemáticas e lógicas.
Funções Variádicas
Funções variádicas em Go são aquelas que podem receber um número variável de argumentos. Essas funções são particularmente úteis quando o número de argumentos não é conhecido antecipadamente ou pode variar. Um exemplo bem conhecido de função variádica em Go é o fmt.Println
, cuja declaração é:
func Println(a ...any{}) (n int, err error)
O parâmetro a ...any
indica que a função pode receber qualquer número de argumentos de qualquer tipo. O operador ...
seguido pelo tipo any
permite que a função aceite zero, um, ou vários argumentos.
Vamos considerar um exemplo que ilustra uma função variádica para somar números inteiros:
package main import "fmt" func soma(nums ...int) (s int) { if len(nums) < 1 { return 0 } for _, i := range nums { s += i } return s } func main() { fmt.Println(soma()) // Retorna 0 fmt.Println(soma(1)) // Retorna 1 fmt.Println(soma(1, 2)) // Retorna 3 fmt.Println(soma(1, 2, 3)) // Retorna 6 fmt.Println(soma(1, 2, 3, 4)) // Retorna 10 }
Neste código, a função soma
aceita um número variável de argumentos do tipo int
. O loop dentro da função itera sobre cada argumento, acumulando a soma em s
.
Interessante notar que funções variádicas também podem ser utilizadas com slices, passando os elementos do slice como argumentos individuais usando o operador ...
. Por exemplo:
... func main() { numeros := []int{1, 2, 3, 4} fmt.Println(soma(numeros...)) }
Essa característica torna as funções variádicas extremamente flexíveis e poderosas, permitindo que sejam usadas em uma ampla variedade de situações.
Função init()
Em Go, a função init()
é usada para inicializar variáveis ou executar configurações necessárias antes da execução de qualquer outra parte do programa. Esta função especial é chamada automaticamente pelo Go antes da função main()
e pode ser usada para preparar o ambiente de execução, como inicializar variáveis globais, configurar conexões de banco de dados, entre outras tarefas.
Aqui está um exemplo clássico do uso da função init()
:
package main import ( "fmt" ) var start = 0 func init() { fmt.Println("This is init()...") start = 10 } func main() { fmt.Println("This is main()...") fmt.Println("Start:", start) }
Ao executar esse código, a saída será:
This is init()...
This is main()...
Start: 10
Isso demonstra que a função init()
é a primeira a ser executada no pacote, configurando o valor da variável start
para 10 antes da execução da função main()
.
Interessante notar que, se houver múltiplas funções init()
dentro do mesmo pacote, todas serão executadas na ordem em que são encontradas no código. Isso pode ser visto em um cenário onde temos dois arquivos no mesmo diretório, cada um com sua própria função init()
. Adicione o código abaixo no mesmo diretório do código anterior:
package main import "fmt" func init() { fmt.Println("This is slacker init()...") }
Como este código está na mesma pasta ele não pode ter uma função main
, pois o Go vai considera que se trata de uma redeclaração da função no mesmo bloco. Em seguida abra um terminal e execute o comando a seguir:
alves@arabel:init$ go run *.go
This is init()…
This is slacker init()…
This is main()…
Start: 10
A função init()
oferece uma maneira conveniente de realizar configurações necessárias antes do início da execução principal do programa. Ela pode ser declarada várias vezes no mesmo pacote, permitindo a organização modular das tarefas de inicialização.
Definindo um Método
Em Go, funções podem ser associadas a structs, funcionando como métodos dessa struct e adicionando um aspecto de orientação a objetos à linguagem. A sintaxe para definir um método é similar à de uma função regular, com a adição de um parâmetro receptor antes do nome da função para especificar a struct à qual o método está associado:
func (r structName) nomeFuncao(inp1, inp2, ...) (out1, out2, ...) {
// corpo da função
}
Como exemplo, o código a seguir define uma struct Retangulo
e associa a ela métodos para calcular sua área e perímetro:
package main import "fmt" type Retangulo struct { larg int comp int } func (r Retangulo) Area() int { return r.comp * r.larg } func (r Retangulo) Perimetro() int { return 2 * (r.larg + r.comp) } func (r *Retangulo) SetRet(larg, comp int) { r.larg = larg r.comp = comp } func main() { r1 := Retangulo{4, 3} fmt.Println(r1) fmt.Println("Área: ", r1.Area()) fmt.Println("Perímetro: ", r1.Perimetro()) r1.SetRet(5, 8) fmt.Println(r1) fmt.Println("Área: ", r1.Area()) fmt.Println("Perímetro: ", r1.Perimetro()) }
Os métodos Area
e Perimetro
são associados à struct Retangulo
. Eles são invocados como métodos do objeto r1
do tipo Retangulo
. Essa abordagem encapsula o comportamento dentro da struct, tornando o código mais organizado e modular.
Alterando Valor do Argumento
Enquanto os métodos Area
e Perimetro
apenas acessam os campos da struct Retangulo
, o método SetRet
altera seus valores. Isso é feito passando o receptor r
como um ponteiro (*Retangulo
), permitindo que o método modifique a instância da struct.
A título de exemplo, considere a função swap
para ilustrar a modificação de valores por meio de argumentos passados como ponteiros:
package main import "fmt" // swap troca os valores das duas variáveis passadas func swap(x, y *int) { z := *x *x = *y *y = z } func main() { i, j := 2, 5 fmt.Println("i:", i, " j:", j) swap(&i, &j) fmt.Println("i:", i, " j:", j) }
Na função swap
, x
e y
são ponteiros para inteiros (*int
). A função troca os valores para os quais x
e y
apontam, demonstrando como funções (ou métodos) em Go podem modificar os valores dos argumentos recebidos.
Considerações Finais
Ao concluirmos este capítulo sobre Structs, Funções e Métodos em Go, temos uma visão abrangente não apenas dos recursos da linguagem, mas também de como esses elementos se entrelaçam para formar a base de um código robusto e eficiente. As structs em Go, embora simples, oferecem um poderoso meio de agrupar dados relacionados, enquanto os métodos associados a estas structs trazem um sabor de orientação a objetos, permitindo que os dados e as operações sobre eles coexistam de forma coesa.
As funções em Go, com sua versatilidade e flexibilidade, desde funções anônimas até variádicas, demonstram a capacidade da linguagem de lidar com uma ampla gama de requisitos de programação. As funções anônimas, em particular, oferecem uma maneira elegante de encapsular lógicas específicas sem a necessidade de poluir o espaço global de nomes, enquanto as funções variádicas proporcionam uma flexibilidade inestimável em situações que exigem um número variável de argumentos.
A função especial init()
em Go desempenha um papel vital na configuração inicial necessária antes do início da execução do programa. Este recurso, embora muitas vezes subestimado, é fundamental para estabelecer as condições adequadas para que o restante do código funcione conforme esperado.
Por fim, a habilidade de modificar valores de argumentos em funções, uma característica fundamental em muitas linguagens de programação, é abordada com clareza em Go, seja através da passagem de argumentos por valor ou por referência. A compreensão desses conceitos é essencial para qualquer desenvolvedor que busca escrever código eficiente e idiomático em Go.
Em resumo, este capítulo destaca a elegância e a potência de Go como uma linguagem de programação. Seja você um iniciante buscando entender os conceitos básicos ou um desenvolvedor experiente procurando aprofundar seu conhecimento, Go oferece um conjunto de ferramentas robusto para construir soluções de software confiáveis e de alto desempenho.
Deixe uma resposta