Método
Un método es una función con un argumento de receptor especial.
func (v Vertex) Abs() float64{
return math.Sqrt(v.X*v.X + v.Y*v.Y)
// In this example, the `Abs` method has a receiver of type `Vertex` named `v`
}
// is equivalent to (in terms of functionality)
func Abs(v Vertex) float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
// You can declare a method on non-struct types, too.
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}
// You can only declare a method with a receiver whose type is defined in the same package as the method. You cannot declare a method with a receiver whose type is defined in another package (which includes the built-in types such as int).
Receptor de puntero
El receptor de un método puede ser un puntero. Esto significa que el tipo de receptor tiene la sintaxis literal *T
de algún tipo T
. (Además, T
no puede ser un puntero como *int
.)
type Vertex struct {
X, Y float64
}
// golang pointer receiver method
func (v* Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f // recall that v.X is the same as dereferencing (*v).X
}
// it can be written as, again, a function
func ScaleFunc(v* Vertex, f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
Dado que los métodos a menudo necesitan modificar su receptor, los receptores de puntero son más comunes que los receptores de valor.
Método con receptor de puntero versus función con un argumento de puntero
Hay una diferencia entre el método con receptor de puntero y una función con argumento de puntero .
// func with ptr arg
var v Vertex
ScaleFunc(v, 5) // compile error
ScaleFunc(&v, 5) // works
// method with ptr receiver
var v Vertex
p := &v
p.Scale(5) // works
v.Scale(5) // also works; auto-referencing
Es decir, por conveniencia, Go interpreta la declaración v.Scale(5)
como si (&v).Scale(5)
el Scale
método tiene un receptor de puntero. Esto no funcionará para las funciones normales.
Lo equivalente sucede en sentido inverso. Lo mismo sucede cuando tienes un método con receptor de valor y una función con un argumento de valor .
type Vertex struct {
X, Y float64
}
func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func AbsFunc(v Vertex) float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
// func with val arg
var v Vertex
fmt.Println(AbsFunc(v)) // works
fmt.Println(AbsFunc(&v)) // compile error
// method with val receiver
var v Vertex
fmt.Println(v.Abs()) // works
p := &v
fmt.Println(p.Abs()) // also works; (*p).Abs(); auto-dereferencing
En este caso, la llamada al método p.Abs()
se interpreta como (*p).Abs()
.
Elegir un valor o un receptor de puntero
Dos razones para utilizar un receptor de puntero:
- El método con un receptor de puntero puede modificar el valor al que apunta su receptor.
- Evite copiar el valor en cada llamada al método. Esto puede ser más eficiente si el receptor es una estructura grande.
type Vertex struct {
X, Y float64
}
func (v *Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
En este ejemplo, aunque el Abs
método no necesita modificar su receptor, es con el tipo de receptor *Vertex
.
En general, todos los métodos de un tipo determinado deben tener receptores de valor o de puntero, pero no una combinación de ambos.
Interfaces
Interfaces básicas
Un tipo de interfaz se define como un conjunto de firmas de métodos.
Un valor de tipo de interfaz puede contener cualquier valor que implemente esos métodos.
type Abser interface {
Abs() float64
}
func main() {
var a1 Abser // a value of Abs() interface type
var a2 Abser // another value of Abs() interface type
f := MyFloat(-1.414) // default constructor
v := Vertex{
3, 4}
a1 = f // a MyFloat implements Abser
a2 = &v // a *Vertex implements Abser
a2 = v // invalid! Vertex (the value type) doesn't implement Abser because the Abs method is defined only on *Vertex (the pointer type).
fmt.Println(a1.Abs())
fmt.Println(a2.Abs())
}
type MyFloat float64
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}
type Vertex struct {
X, Y float64
}
func (v *Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
Las interfaces se implementan implícitamente
Un tipo implementa una interfaz implementando sus métodos. No hay una declaración explícita de intenciones, ni una palabra clave "implements".
Las interfaces implícitas desacoplan la definición de una interfaz de su implementación, que luego podría aparecer en cualquier paquete sin un arreglo previo.
Valores de interfaz
Bajo el capó, los valores de interfaz se pueden considerar como una tupla de un valor y un tipo concreto:
(value, type)
un valor de interfaz contiene un valor de un tipo concreto subyacente específico.
Llamar a un método en un valor de interfaz ejecuta el método del mismo nombre en su tipo subyacente.
Valores de interfaz con valores subyacentes nulos
type I interface {
M()
}
type T struct {
S string
}
func (t *T) M() {
if t == nil {
fmt.Println("<nil>")
return
}
fmt.Println(t.S)
}
func main() {
var i I
var t *T
i = t
describe(i)
i.M() // If the concrete value inside the interface itself is nil, the method will be called with a nil receiver.
i = &T{
"hello"}
describe(i) // Note that an interface value that holds a nil concrete value is itself non-nil.
i.M()
}
func describe(i I) {
fmt.Printf("(%v, %T)\n", i, i)
}
En algunos lenguajes, esto desencadenaría una excepción de puntero nulo, pero en Go es común escribir métodos que manejen con gracia las llamadas con un receptor nulo (como con el método M
en este ejemplo).
Un nil
valor de interfaz no tiene valor ni tipo concreto.
Llamar a un método en una interfaz nula es un error en tiempo de ejecución porque no hay ningún tipo dentro de la tupla de la interfaz para indicar a qué método concreto llamar.
type I interface {
M()
}
func main() {
var i I
describe(i)
i.M() // panic: runtime error: invalid memory address or nil pointer dereference, i is nil itself!
}
func describe(i I) {
fmt.Printf("(%v, %T)\n", i, i)
}
La interfaz vacía
El tipo de interfaz que especifica métodos cero se conoce como interfaz vacía:
interface{}
una interfaz vacía puede contener valores de cualquier tipo. (Cada tipo implementa al menos cero métodos).
func main() {
var i interface{
}
describe(i)
i = 42
describe(i)
i = "hello"
describe(i)
}
func describe(i interface{
}) {
fmt.Printf("(%v, %T)\n", i, i)
}
Las interfaces vacías son utilizadas por código que maneja valores de tipo desconocido. Por ejemplo, fmt.Print
toma cualquier número de argumentos de tipo interface{}
.
Tipo de aserciones
Una aserción de tipo proporciona acceso al valor concreto subyacente de un valor de interfaz.
func main() {
var i interface{
} = "hello"
s := i.(string) // type assertion; this statement asserts that the interface value i holds the concrete type string and assigns the underlying string value to the variable s.
fmt.Println(s)
s, ok := i.(string)
fmt.Println(s, ok) // "hello" and true
f, ok := i.(float64)
fmt.Println(f, ok) // 0 and false
f = i.(float64) // panic!
}
Tipo de interruptor
func do(i interface{
}) {
switch v := i.(type) {
case int:
fmt.Printf("Twice %v is %v\n", v, v*2)
case string:
fmt.Printf("%q is %v bytes long\n", v, len(v))
default:
fmt.Printf("I don't know about type %T!\n", v)
}
}
func main() {
do(21)
do("hello")
do(true)
}
Largueros
El paquete Stringer
define una de las interfaces más ubicuas fmt
.
type Stringer interface {
String() string
}
A Stringer
es un tipo que puede describirse a sí mismo como una cadena. El fmt
paquete (y muchos otros) busca esta interfaz para imprimir valores.
type IPAddr [4]byte
// TODO: Add a "String() string" method to IPAddr.
type Stringer interface {
String() string
}
func (ip IPAddr) String() string {
return fmt.Sprintf("%v.%v.%v.%v", ip[0], ip[1], ip[2], ip[3])
}
func main() {
hosts := map[string]IPAddr{
"loopback": {
127, 0, 0, 1},
"googleDNS": {
8, 8, 8, 8},
}
for name, ip := range hosts {
fmt.Printf("%v: %v\n", name, ip)
}
}
Errores
Los programas Go expresan el estado de error con error
valores.
El error
tipo es una interfaz integrada similar a fmt.Stringer
:
type error interface {
Error() string
}
Al igual que con fmt.Stringer
, el paquete fmt busca la error
interfaz al imprimir valores.
Las funciones a menudo devuelven un error
valor y el código de llamada debe manejar los errores probando si el error es igual nil
.
i, err := strconv.Atoi("42")
if err != nil {
fmt.Printf("couldn't convert number: %v\n", err)
return
}
fmt.Println("Converted integer:", i)
A nil error
denota éxito; a non-nil error
denota fracaso.
type ErrNegativeSqrt float64
func (e ErrNegativeSqrt) Error() string {
return fmt.Sprintf("cannot Sqrt negative number: %v\n", float64(e))
}
func Sqrt(x float64) (float64, error) {
if x < 0 {
var e ErrNegativeSqrt = ErrNegativeSqrt(x)
return -1, e
}
return math.Sqrt(x), nil
}
func main() {
fmt.Println(Sqrt(2))
fmt.Println(Sqrt(-2))
}
Lectores
package main
import (
"fmt"
"io"
"strings"
)
func NewReader(s string) *Reader // this is from "strings" package. NewReader returns a new Reader reading from s. It is similar to bytes.NewBufferString but more efficient and read-only.
func main)() {
r := strings.NewReader("Hello, Reader!")
b := make([]byte, 8) // init a byte slice of size 8, i.e., 8 bytes
/*
type Reader interface {
Read(p []byte) (n int, err error)
}
Read reads up to len(p) bytes into p. It returns the number of bytes read (0 <= n <= len(p)) and any error encountered.
*/
for {
n, err := r.Read(b)
fmt.Printf("n = %v, err = %v, b = %v\n", n, err, b)
fmt.Printf("b[:n] = %q\n", b[:n])
if err == io.EOF {
break
}
}
}
Conceptos omitidos: imágenes
Las imágenes se omiten por ahora.