Go数据类型

Scroll Down

Go语言数据类型

布尔型

布尔型的值只可以是常量 true 或者 false。一个简单的例子:var b bool = true。

数字类型

整型 int 和浮点型 float32、float64,Go 语言支持整型和浮点型数字,并且支持复数,其中位的运算采用补码。

go的数字类型有多种:
int、int8、int16、int32、int64
uint、uint8、uint16、uint32、uint64、uintptr
byte // uint8 的别名
rune // int32 的别名 代表一个 Unicode 码
float32、float64
complex64、complex128
详细解释参考go语言数据类型

字符串类型

字符串就是一串固定长度的字符连接起来的字符序列。Go 的字符串是由单个字节连接起来的。Go 语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本。

派生类型

指针类型(pointer)

什么是指针,指针变量就是一个指向了一个值的内存地址
指针可以指向任何类型,通过*+type声明

var var_name *var-type
#或者
var var_name = new(类型)

例如

var x *int32 //声明一个变量x是int32类型的指针
var y *[4]int //声明一个变量y是int数组的指针
var z *[]byte //声明一个变量z是byte切片类型的指针
#或者
#定义一个string类型的指针变量str
str := new(string)
*str = "Go语言教程"
fmt.Println(*str)

new() 函数可以创建一个对应类型的指针,创建过程会分配内存,被创建的指针指向默认值

如何使用指针
指针使用流程:

  • 定义指针变量。
  • 为指针变量赋值。
  • 访问指针变量中指向地址的值
package main

import "fmt"

func main() {
   var a int= 20   /* 声明实际变量 */
   var ip *int        /* 声明指针变量 */
   ip = &a  /* 指针变量的存储地址 */
   fmt.Printf("a 变量的地址是: %x\n", &a  )
   /* 指针变量的存储地址 */
   fmt.Printf("ip 变量储存的指针地址: %x\n", ip )
   /* 使用指针访问值 */
   fmt.Printf("*ip 变量的值: %d\n", *ip )
}

输出结果:

a 变量的地址是: 20818a220
ip 变量储存的指针地址: 20818a220
*ip 变量的值: 20

空指针
当一个指针被定义后没有分配到任何变量时,它的值为nil。nil 指针也称为空指针。nil在概念上和其它语言的null、None、nil、NULL一样,都指代零值或空值。一个指针变量通常缩写为ptr。
Go语言&和*
&:在Go语言中可以通过&来获取一个变量的内存地址,从而将获取到的内存地址赋值给到指针变量。
*:Go语言可以通过在指针类型前添加一个*来获取指针类型所指向的内容。
示例:

package main

import "fmt"

func main() {
   var a int= 20   /* 声明实际变量 */
   var ip *int        /* 声明指针变量 */

   ip = &a  /* 指针变量的存储地址 */

   fmt.Printf("a 变量的地址是: %x\n", &a  )

   /* 指针变量的存储地址 */
   fmt.Printf("ip 变量储存的指针地址: %x\n", ip )

   /* 使用指针访问值 */
   fmt.Printf("*ip 变量的值: %d\n", *ip )
}

输出结果:

a 变量的地址是: 20818a220
ip 变量储存的指针地址: 20818a220
*ip 变量的值: 20

数组类型([])

Go语言提供了数组类型的数据结构。和其他语言数据类似但是Go语言数据声明必须指定长度。数据的索引从0开始,可以通过索引下标来获取数组中的具体值。数组可以定义任何类型,例如整形,字符串或者自定义类型。数组一旦声明长度不可以改变。
声明数组

var variable_name [SIZE] variable_type

例如

var balance [10] float32

初始化数组
初始化数组中 {} 中的元素个数不能大于 [] 中的数字。如果忽略 [] 中的数字不设置数组大小,Go 语言会根据元素的个数来设置数组的大小。

var balance = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
var balance = [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}

数组示例

package main

import "fmt"

func main() {
   var n [10]int /* n 是一个长度为 10 的数组 */
   var i,j int
   /* 为数组 n 初始化元素 */        
   for i = 0; i < 10; i++ {
      n[i] = i + 100 /* 设置元素为 i + 100 */
   }
   /* 输出每个数组元素的值 */
   for j = 0; j < 10; j++ {
      fmt.Printf("Element[%d] = %d\n", j, n[j] )
   }
}

输出结果

Element[0] = 100
Element[1] = 101
Element[2] = 102
Element[3] = 103
Element[4] = 104
Element[5] = 105
Element[6] = 106
Element[7] = 107
Element[8] = 108
Element[9] = 109

结构体类型(struct)

Go语言可以通过创建结构体来自定义类型。这里的类型在形式上类似与Java里面的类的概念,但是也是仅仅是形式上我们可以使用Java里面类的概念来立即结构体(Java程序员固化思维请勿吐槽)。

在结构体中可以定义一个或多个数据类型,其中的每一个值都称为结构体的成员。结构体的成员也可以称为“字段”,字段一般有一下特性:

  • 字段拥有自己的类型和值;
  • 字段名在结构体中是唯一的;
  • 字段的类型也可以是结构体,甚至是字段所在结构体的类型;
    结构体的声明和定义,可以通过关键字type和struct{}结构来声明一个结构体。
type struct_variable_type struct {
   member definition
   member definition
   ...
   member definition
}

结构体一旦定义就可以被其他变量来使用。
示例1:声明一个结构体名为Books,具体title,author,subject,book_id属性。

type Books struct {
   title string
   author string
   subject string
   book_id int
}

示例2:创建一个结构体Books的实例对象,调用结构体的属性

package main

import "fmt"

type Books struct {
   title string
   author string
   subject string
   book_id int
}


func main() {
    // 创建一个新的结构体
    fmt.Println(Books{"Go 语言", "www.runoob.com", "Go 语言教程", 6495407})
    // 也可以使用 key => value 格式
    fmt.Println(Books{title: "Go 语言", author: "www.runoob.com", subject: "Go 语言教程", book_id: 6495407})
    // 忽略的字段为 0 或 空
   fmt.Println(Books{title: "Go 语言", author: "www.runoob.com"})
   var Book1 Books
   /* book 1 描述 */
   Book1.title = "Go 语言"
   Book1.author = "www.runoob.com"
   Book1.subject = "Go 语言教程"
   Book1.book_id = 6495407
   fmt.Printf( "Book 1 title : %s\n", Book1.title)
   fmt.Printf( "Book 1 author : %s\n", Book1.author)
   fmt.Printf( "Book 1 subject : %s\n", Book1.subject)
   fmt.Printf( "Book 1 book_id : %d\n", Book1.book_id)
}

结果输出:

{Go 语言 www.runoob.com Go 语言教程 6495407}
{Go 语言 www.runoob.com Go 语言教程 6495407}
{Go 语言 www.runoob.com 0}
Book 1 title : Go 语言
Book 1 author : www.runoob.com
Book 1 subject : Go 语言教程
Book 1 book_id : 6495407

当结构体被定义之后,结构体就和其他数据复合数据类型一样,我们也可以通过定义结构体的类型的指针。

#定义一个指针变量ptr用来存放Books类型的指针
var ptr *Books

#给ptr赋值
ptr = &Book1

以上描述了结构体的成员属性,结构体同时可以创建成员方法。Go语言中的方法,是一种特殊的函数,定义于struct之上(与struct关联、绑定),被称为struct的receiver。
示例:定义一个结构体xingzhuang,并且声明结构体的方法area(),方法area属于结构体xingzhuang,可以通过类型xingzhuang的实例c来调用方法area。

package main

import "fmt"
 
type xingzhuang struct {
	length float64
	width  float64
}
 
func (c *xingzhuang) area() float64 {
	return c.length * c.width
}
 
func main() {
	c := &xingzhuang{
		2.5,
		4.0,
	}
	fmt.Printf("%f\n",c.area())
}

通道类型(channel)

channel 是Go语言在语言级别提供的 goroutine 间的通信方式。我们可以使用 channel 在两个或多个 goroutine 之间传递消息。

channel 是类型相关的,也就是说,一个 channel 只能传递一种类型的值,这个类型需要在声明 channel 时指定。如果对 Unix 管道有所了解的话,就不难理解 channel,可以将其认为是一种类型安全的管道。

创建通道

定义一个 channel 时,也需要定义发送到 channel 的值的类型,注意,创建时必须使用 make 创建 channel,创建时候要结合chan关键字一起使用。

#声明一个通道不需要make
var 通道变量 chan 通道类型
var ci chan int

#创建通道
通道变量 := make(chan 通道类型)

ci := make(chan int) //定义一个int类型通道
cs := make(chan string) //定义一个string类型通道
cf := make(chan interface{}) //定义一个任意类型通道

type Equip struct{ /* 一些字段 */ }
ch2 := make(chan *Equip) // 创建Equip指针类型的通道, 可以存放*Equip

向通道发送数据
Go语言使用符号’<-‘向通道发送数据。

通道变量 <- 值

// 创建一个空接口通道
ch := make(chan interface{})
// 将0放入通道中
ch <- 0
// 将hello字符串放入通道中
ch <- "hello"

把数据往通道中发送时,如果接收方一直都没有接收,那么发送操作将持续阻塞。Go 程序运行时能智能地发现一些永远无法发送成功的语句并做出提示,示例如下:

package main
func main() {
    // 创建一个整型通道
    ch := make(chan int)
    // 尝试将0通过通道发送
    ch <- 0
}

输出结果:

fatal error: all goroutines are asleep - deadlock!

通道接收数据
Go语言也是使用符号’<-‘接受通道的数据。

#接受通道ch中的数据给变量data
data := <-ch //阻塞接受通道数据

#data表示接收到的数据。未接收到数据时,data 为通道类型的零值;ok:表示是否接收到数据。
data, ok := <-ch //非阻塞接收数据

#阻塞接受数据,执行该语句时将会发生阻塞,直到接收到数据,但接收到的数据会被忽略
<-ch

示例:

package main
import (
    "fmt"
)
func main() {
    // 构建一个通道
    ch := make(chan int)
    // 开启一个并发匿名函数
    go func() {
        fmt.Println("start goroutine")
        // 通过通道通知main的goroutine
        ch <- 0
        fmt.Println("exit goroutine")
    }()
    fmt.Println("wait goroutine")
    // 等待匿名goroutine
    <-ch
    fmt.Println("all done")
}

输出结果:

wait goroutine
start goroutine
exit goroutine
all done

注意: Go语言的通道默认是无缓冲区的通道,也就是说通道发送和接受要同时准备好,否则发送/接受通道会进入阻塞等待。
创建带有缓冲区的通道。缓冲区大小不可以指定过大或者无限大。否则当消费放通道出现问题时候,如果通道不限制长度,那么内存将不断膨胀直到应用崩溃。

通道实例 := make(chan 通道类型, 缓冲大小)

#示例
package main
import "fmt"
func main() {
    // 创建一个3个元素缓冲大小的整型通道
    ch := make(chan int, 3)
    // 查看当前通道的大小
    fmt.Println(len(ch))
    // 发送3个整型元素到通道
    ch <- 1
    ch <- 2
    ch <- 3
    // 查看当前通道的大小
    fmt.Println(len(ch))
}

输出结果:

0
3

单向通道

单向通道是指将其指定为单向 channel 变量,从而限制该函数中可以对此 channel 的操作,比如只能往这个 channel 中写入数据,或者只能从这个 channel 读取数据。

单向 channel 变量的声明非常简单,只能写入数据的通道类型为chan<-,只能读取数据的通道类型为<-chan,格式如下:

var 通道实例 chan<- 元素类型    // 只能写入数据的通道
var 通道实例 <-chan 元素类型    // 只能读取数据的通道

ch := make(chan int)
// 声明一个只能写入数据的通道类型, 并赋值为ch
var chSendOnly chan<- int = ch
//声明一个只能读取数据的通道类型, 并赋值为ch
var chRecvOnly <-chan int = ch

函数类型(function)

函数定义

Go语言函数由函数名,参数列表,返回值,函数体组成。如果返回值有多个,用()包住所有返回值类型定义。

func 函数名(形式参数列表)(返回值列表){
    函数体
}

示例1: 函数声明

/* 函数返回两个数的最大值 */
func max(num1, num2 int) int {
   /* 声明局部变量 */
   var result int

   if (num1 > num2) {
      result = num1
   } else {
      result = num2
   }
   return result
}

示例2: 在 main() 函数中调用 max()函数

package main

import "fmt"

func main() {
   /* 定义局部变量 */
   var a int = 100
   var b int = 200
   var ret int

   /* 调用函数并返回最大值 */
   ret = max(a, b)

   fmt.Printf( "最大值是 : %d\n", ret )
}

/* 函数返回两个数的最大值 */
func max(num1, num2 int) int {
   /* 定义局部变量 */
   var result int

   if (num1 > num2) {
      result = num1
   } else {
      result = num2
   }
   return result
}

输出结果:

最大值是 : 200

示例3: 返回多个值

package main

import "fmt"

func swap(x, y string) (string, string) {
   return y, x
}

func main() {
   a, b := swap("Google", "Runoob")
   fmt.Println(a, b)
}

输出结果:

Runoob Google

示例4: 多个输入参数
4.1 多个输入参数

func myfunc(args ...int) {
    for _, arg := range args {
        fmt.Println(arg)
    }
}

输出结果:

myfunc(2, 3, 4)
myfunc(1, 3, 7, 13)

4.2 多个不同类型的输入参数
如果你希望传任意类型,可以指定类型为 interface{},声明如下:

func Printf(args ...interface{}) {
    // ...
}
package main
import "fmt"
func MyPrintf(args ...interface{}) {
    for _, arg := range args {
        switch arg.(type) {
            case int:
                fmt.Println(arg, "is an int value.")
            case string:
                fmt.Println(arg, "is a string value.")
            case int64:
                fmt.Println(arg, "is an int64 value.")
            default:
                fmt.Println(arg, "is an unknown type.")
        }
    }
}
func main() {
    var v1 int = 1
    var v2 int64 = 234
    var v3 string = "hello"
    var v4 float32 = 1.234
    MyPrintf(v1, v2, v3, v4)
}

输出结果:

1 is an int value.
234 is an int64 value.
hello is a string value.
1.234 is an unknown type.

示例5: 函数作为返回值

package main
import "fmt"
func main() {
    // make an Add2 function, give it a name p2, and call it:
    p2 := Add2()
    fmt.Printf("Call Add2 for 3 gives: %v\n", p2(3))
    // make a special Adder function, a gets value 3:
    TwoAdder := Adder(2)
    fmt.Printf("The result is: %v\n", TwoAdder(3))
}
func Add2() func(b int) int {
    return func(b int) int {
        return b + 2
    }
}
func Adder(a int) func(b int) int {
    return func(b int) int {
        return a + b
    }
}

输出结果:

Call Add2 for 3 gives: 5
The result is: 5

匿名函数

匿名函数定义

和一般函数定义的区别:匿名函数没有方法名

func(参数列表)(返回参数列表){
    函数体
}

示例1: 匿名函数调用

func(data int) {
    fmt.Println("hello", data)
}(100)

输出结果:

hello 100

示例2: 匿名函数赋值给变量
利用这个特点可以,可以保存匿名函数作为回调。

// 将匿名函数体保存到f()中
f := func(data int) {
    fmt.Println("hello", data)
}
// 使用f()调用
f(100)

示例3: 匿名函数用作回调函数

package main
import (
    "fmt"
)
// 遍历切片的每个元素, 通过给定函数进行元素访问
func visit(list []int, f func(int)) {
    for _, v := range list {
        f(v)
    }
}
func main() {
    // 使用匿名函数打印切片内容
    visit([]int{1, 2, 3, 4}, func(v int) {
        fmt.Println(v)
    })
}

代码说明如下:

  • 第 8 行,使用 visit() 函数将整个遍历过程进行封装,当要获取遍历期间的切片值时,只需要给 visit() 传入一个回调参数即可。
  • 第 18 行,准备一个整型切片 []int{1,2,3,4} 传入 visit() 函数作为遍历的数据。
  • 第 19~20 行,定义了一个匿名函数,作用是将遍历的每个值打印出来。

Go语言闭包

Go语言中闭包是引用了自由变量的函数,被引用的自由变量和函数一同存在,即使已经离开了自由变量的环境也不会被释放或者删除,在闭包中可以继续使用这个自由变量。
示例: 闭包特性的变量记忆效应

package main
import (
    "fmt"
)
// 提供一个值, 每次调用函数会指定对值进行累加
func Accumulate(value int) func() int {
    // 返回一个闭包
    return func() int {
        // 累加
        value++
        // 返回一个累加值
        return value
    }
}
func main() {
    // 创建一个累加器, 初始值为1
    accumulator := Accumulate(1)

    // 累加1并打印
    fmt.Println(accumulator())
    fmt.Println(accumulator())
    // 打印累加器的函数地址
    fmt.Printf("%p\n", &accumulator)
    // 创建一个累加器, 初始值为1
    accumulator2 := Accumulate(10)
    // 累加1并打印
    fmt.Println(accumulator2())
    // 打印累加器的函数地址
    fmt.Printf("%p\n", &accumulator2)
}

输出结果:

2
3
0xc00000e028
11
0xc00000e038

defer延迟执行语句

Go语言的 defer 语句会将其后面跟随的语句进行延迟处理,在 defer 归属的函数即将返回时,将延迟处理的语句按 defer 的逆序进行执行,也就是说,先被 defer 的语句最后被执行,最后被 defer 的语句,最先被执行。

示例1:
当有多个 defer 行为被注册时,它们会以逆序执行(类似栈,即后进先出)

package main
import (
    "fmt"
)
func main() {
    fmt.Println("defer begin")
    // 将defer放入延迟调用栈
    defer fmt.Println(1)
    defer fmt.Println(2)
    // 最后一个放入, 位于栈顶, 最先调用
    defer fmt.Println(3)
    fmt.Println("defer end")
}

输出结果:

defer begin
defer end
3
2
1

利用defer的特性可以用来进行并发锁的释放操作,可以在众多操作执行完之后,在执行锁的unlock()操作

func readValue(key string) int {
    valueByKeyGuard.Lock()
    // defer后面的语句不会马上调用, 而是延迟到函数结束时调用
    defer valueByKeyGuard.Unlock()
    
    //模拟其他操作
    ...
    return valueByKey[key]
}

切片类型(slice)

Go 语言切片是对数组的抽象。
Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go中提供了一种灵活,功能强悍的内置类型切片("动态数组"),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。
声明一个切片,切片不需要说明长度。或使用make()函数来创建切片:

#1.通过var声明
var identifier []type

#2.通过make创建切片,没有指定容量,那么默认容量=长度
var slice1 []type = make([]type, len)
#或者简写
slice1 := make([]type, len)

#3.也可以指定切片容量,capacity表示容量
slice1 := make([]T, length, capacity)

切片容量和长度
长度:切片的长度是说,切片中所包含的元素个数
容量:切片容量是说,从第一个元素开始数开始到其底层数组最后一个元素末尾的个数

空切片
一个切片在未初始化之前是一个nil,长度为0

package main

import "fmt"

func main() {
   var numbers []int

   printSlice(numbers)

   if(numbers == nil){
      fmt.Printf("切片是空")
   }
}

func printSlice(x []int){
   fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}

输出结果

len=0 cap=0 slice=[]
切片是空

切片截取
切片默认指向一段连续内存区域,可以是数组,也可以是切片本身。从连续内存区域生成切片是常见的操作,格式如下:

slice [开始位置 : 结束位置]

语法说明如下:
slice:表示目标切片对象;
开始位置:对应目标切片对象的索引;
结束位置:对应目标切片的结束索引。

示例代码:

func main() {
   /* 创建切片 */
   numbers := []int{0,1,2,3,4,5,6,7,8}  
   printSlice(numbers)

   /* 打印原始切片 */
   fmt.Println("numbers ==", numbers)

   /* 打印子切片从索引1(包含) 到索引4(不包含)*/
   fmt.Println("numbers[1:4] ==", numbers[1:4])

   /* 默认下限为 0*/
   fmt.Println("numbers[:3] ==", numbers[:3])

   /* 默认上限为 len(s)*/
   fmt.Println("numbers[4:] ==", numbers[4:])

   numbers1 := make([]int,0,5)
   printSlice(numbers1)

   /* 打印子切片从索引  0(包含) 到索引 2(不包含) */
   number2 := numbers[:2]
   printSlice(number2)

   /* 打印子切片从索引 2(包含) 到索引 5(不包含) */
   number3 := numbers[2:5]
   printSlice(number3)

}

func printSlice(x []int){
   fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}

输出结果:

len=9 cap=9 slice=[0 1 2 3 4 5 6 7 8]
numbers == [0 1 2 3 4 5 6 7 8]
numbers[1:4] == [1 2 3]
numbers[:3] == [0 1 2]
numbers[4:] == [4 5 6 7 8]
len=0 cap=5 slice=[]
len=2 cap=9 slice=[0 1]
len=3 cap=7 slice=[2 3 4]

切片动态扩展和复制
Go语言通过append()和copy()实现对切片的动态扩展和复制。

不过需要注意的是,在使用 append() 函数为切片动态添加元素时,如果空间不足以容纳足够多的元素,切片就会进行“扩容”,此时新切片的长度会发生改变,当发生扩容之后,切片最终的容量不一定会等于切片的长度
切片在扩容时,容量的扩展规律是按容量的 2 倍数进行扩充,例如 1、2、4、8、16……,代码如下:

var numbers []int
for i := 0; i < 10; i++ {
    numbers = append(numbers, i)
    fmt.Printf("len: %d  cap: %d pointer: %p\n", len(numbers), cap(numbers), numbers)
}

输出结果:

len: 1 cap: 1 pointer: 0xc0420080e8
len: 2 cap: 2 pointer: 0xc042008150
len: 3 cap: 4 pointer: 0xc04200e320
len: 4 cap: 4 pointer: 0xc04200e320
len: 5 cap: 8 pointer: 0xc04200c200
len: 6 cap: 8 pointer: 0xc04200c200
len: 7 cap: 8 pointer: 0xc04200c200
len: 8 cap: 8 pointer: 0xc04200c200
len: 9 cap: 16 pointer: 0xc042074000
len: 10 cap: 16 pointer: 0xc042074000

通过最终结果可以看到切片在通过append()在末尾动态添加元素的时候,发生了自动扩容,但是最终的cap并不会等于len。

切片除了可以在尾部通过append()方法扩展元素,可以直接插入元素到头部,但是要注意的是,在切片开头添加元素一般都会导致内存的重新分配,而且会导致已有元素全部被复制 1 次,因此,从切片的开头添加元素的性能要比从尾部追加元素的性能差很多

copy()方法的使用方法

#复制source切片到target切片中
copy(target, source)

示例代码:

package main

import "fmt"

func main() {
   var numbers []int
   printSlice(numbers)

   /* 允许追加空切片 */
   numbers = append(numbers, 0)
   printSlice(numbers)

   /* 向切片添加一个元素 */
   numbers = append(numbers, 1)
   printSlice(numbers)

   /* 同时添加多个元素 */
   numbers = append(numbers, 2,3,4)
   printSlice(numbers)

   /* 创建切片 numbers1 是之前切片的两倍容量*/
   numbers1 := make([]int, len(numbers), (cap(numbers))*2)

   /* 拷贝 numbers 的内容到 numbers1 */
   copy(numbers1,numbers)
   printSlice(numbers1)  
}

func printSlice(x []int){
   fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}

输出结果:

len=0 cap=0 slice=[]
len=1 cap=1 slice=[0]
len=2 cap=2 slice=[0 1]
len=5 cap=6 slice=[0 1 2 3 4]
len=5 cap=12 slice=[0 1 2 3 4]

接口类型(interface)

Go语言也提供了接口这样的数据类型,接口类型是对其它类型行为的抽象和概括;因为接口类型不会和特定的实现细节绑定在一起,通过这种抽象的方式我们可以让我们的函数更加灵活和更具有适应能力。其实也没有太多需要解释的,思想上Go语言的接口也是对特定方法的抽象和规范,具体的实现方式交给实现方去做,接口只是定义了方法和约束。
接口定义:

/* 定义接口 */
type interface_name interface {
   method_name1 [return_type]
   method_name2 [return_type]
   method_name3 [return_type]
   ...
   method_namen [return_type]
}

/* 定义结构体 */
type struct_name struct {
   /* variables */
}

/* 实现接口方法 */
func (struct_name_variable struct_name) method_name1() [return_type] {
   /* 方法实现 */
}

func (struct_name_variable struct_name) method_namen() [return_type] {
   /* 方法实现*/
}

示例
我们定义了一个接口Phone,接口里面有一个方法call()。然后我们在main函数里面定义了一个Phone类型变量,并分别为之赋值为NokiaPhone和IPhone。然后调用call()方法,输出结果如下:

package main

import (
    "fmt"
)

type Phone interface {
    call()
}

type NokiaPhone struct {
}

func (nokiaPhone NokiaPhone) call() {
    fmt.Println("I am Nokia, I can call you!")
}

type IPhone struct {
}

func (iPhone IPhone) call() {
    fmt.Println("I am iPhone, I can call you!")
}

func main() {
    var phone Phone

    phone = new(NokiaPhone)
    phone.call()

    phone = new(IPhone)
    phone.call()

}

输出结果:

I am Nokia, I can call you!
I am iPhone, I can call you!

示例2:
Go语言提供的io包中提供的 Writer 接口:这个接口可以调用 Write() 方法写入一个字节数组([]byte),返回值告知写入字节数(n int)和可能发生的错误(err error)。

type Writer interface {
    Write(p []byte) (n int, err error)
}

查看了上面几个例子之后,我们肯定会有疑问,Go也没有类似于Java中的implements的关键字来明确,我们都是根据方法来判定的,接口可以被其他类型实现,那么我们怎么判定一个类型是否实现了接口呢?

接口实现类判定: 如果一个任意类型 T的方法集为一个接口类型的方法集的超集,则我们说类型 T 实现了此接口类型。T 可以是一个非接口类型,也可以是一个接口类型。也就是说只要类型T的所有方法包含接口X的所有方法,那么我们就可以判定T实现了接口X。也就是说类型T的所有方法肯定是>=X的所有方法的。

接口实现注意两点
1.接口的方法与实现接口的类型方法格式一致。在类型中添加与接口签名一致的方法就可以实现该方法。签名包括方法中的名称、参数列表、返回参数列表。也就是说,只要实现接口类型中的方法的名称、参数列表、返回参数列表中的任意一项与接口要实现的方法不一致,那么接口的这个方法就不会被实现。
2.接口中所有方法均被实现。哪怕这个方法我们不实际使用,那么也要实现,这点类似与Java的接口实现。
**3.类型和接口之间有一对多和多对一的关系。**也就是说一个类型可以实现多个接口或者一个接口可以被多个类型实现。

接口和nil
nil 在 Go语言中只能被赋值给指针和接口。接口在底层的实现有两个部分:type 和 data。在源码中,显式地将 nil 赋值给接口时,接口的 type 和 data 都将为 nil。此时,接口与 nil 值判断是相等的。但如果将一个带有类型的 nil 赋值给接口时,只有 data 为 nil,而 type 为 nil,此时,接口与 nil 判断将不相等。
示例
声明接口

type Stringer interface {
    String() string
}

接口实现

package main
import "fmt"
// 定义一个结构体
type MyImplement struct{}
// 实现fmt.Stringer的String方法
func (m *MyImplement) String() string {
    return "hi"
}
// 在函数中返回fmt.Stringer接口
func GetStringer() fmt.Stringer {
    // 赋nil
    var s *MyImplement = nil
    // 返回变量
    return s
}
func main() {
    // 判断返回值是否为nil
    if GetStringer() == nil {
        fmt.Println("GetStringer() == nil")
    } else {
        fmt.Println("GetStringer() != nil")
    }
}

代码解释:

  • 第 9 行,实现 fmt.Stringer 的 String() 方法。
  • 第 21 行,s 变量此时被 fmt.Stringer 接口包装后,实际类型为*MyImplement,值为 nil 的接口。
  • 第 27 行,使用 GetStringer() 的返回值与 nil 判断时,虽然接口里的 value 为 nil,但 type 带有 *MyImplement 信息,使用 == 判断相等时,依然不为 nil。

输出结果:

GetStringer() != nil

为了避免这类误判的问题,可以在函数返回时,发现带有 nil 的指针时直接返回 nil,代码如下:

func GetStringer() fmt.Stringer {
    var s *MyImplement = nil
    if s == nil {
        return nil
    }
    return s
}

空接口类型interface{}
空接口是接口类型的特殊形式,空接口没有任何方法,因此任何类型都无须实现空接口。从实现的角度看,任何值都满足这个接口的需求。因此空接口类型可以保存任何值,也可以从空接口中取出原值。空接口类型可以很大程度上帮助我们编程,我们可以将空接口类型interface{},看成是Java类型里面的Object。可以匹配任意类型。

空接口的内部实现保存了对象的类型和指针。使用空接口保存一个数据的过程会比直接用数据对应类型的变量保存稍慢。因此在开发中,应在需要的地方使用空接口,而不是在所有地方使用空接口。
示例:

#声明一个空接口
var any interface{}
any = 1
fmt.Println(any)
any = "hello"
fmt.Println(any)
any = false
fmt.Println(any)

输出结果:

1
hello
false

注意:
空接口的值比较,空接口在保存不同的值后,可以和其他变量值一样使用==进行比较操作。空接口的比较有以下几种特性:

  1. 类型不同的空接口间的比较结果不相同。这点比较容易理解,因为接口包含类型(type)和接口数据(data),因此当类型不同的时候,那么==比较也不会相同
    例如
// a保存整型
var a interface{} = 100
// b保存字符串
var b interface{} = "hi"
// 两个空接口不相等
fmt.Println(a == b)

输出结果:

false

  1. 不能比较空接口中的动态值,例如不要比较切片
    例如:
// c保存包含10的整型切片
var c interface{} = []int{10}
// d保存包含20的整型切片
var d interface{} = []int{20}
// 这里会发生崩溃
fmt.Println(c == d)

最终比较程序报错:

panic: runtime error: comparing uncomparable type []int

error接口
Java中有Exception来处理程序中的错误,Go语言中引入 error 接口类型作为错误处理的标准模式,如果函数要返回错误,则返回值类型列表中肯定包含 error。

error基本用法
error 接口有一个签名为 Error() string 的方法,所有实现该接口的类型都可以当作一个错误类型。Error() 方法给出了错误的描述,在使用 fmt.Println 打印错误时,会在内部调用 Error() string 方法来得到该错误的描述。

一般情况下,如果函数需要返回错误,就将 error 作为多个返回值中的最后一个(但这并非是强制要求)。

package main
import (
    "errors"
    "fmt"
    "math"
)
func Sqrt(f float64) (float64, error) {
    if f < 0 {
        return -1, errors.New("math: square root of negative number")
    }
    return math.Sqrt(f), nil
}
func main() {
    result, err := Sqrt(-13)
    if err != nil {
        fmt.Println(err)
    } else {
        fmt.Println(result)
    }
}

输出结果:

math: square root of negative number

自定义error处理示例:

package main
import (
    "fmt"
    "math"
)
type dualError struct {
    Num     float64
    problem string
}
func (e dualError) Error() string {
    return fmt.Sprintf("Wrong!!!,because \"%f\" is a negative number", e.Num)
}
func Sqrt(f float64) (float64, error) {
    if f < 0 {
        return -1, dualError{Num: f}
    }
    return math.Sqrt(f), nil
}
func main() {
    result, err := Sqrt(-13)
    if err != nil {
        fmt.Println(err)
    } else {
        fmt.Println(result)
    }
}

输出结果:

Wrong!!!,because "-13.000000" is a negative number

Go语言断言
类型断言(Type Assertion)是一个使用在接口值上的操作,用于检查接口类型变量所持有的值是否实现了期望的接口或者具体的类型。更多Go断言解释

在Go语言中类型断言的语法格式如下:

value, ok := x.(T)

其中,x 表示一个接口的类型,T 表示一个具体的类型(也可为接口类型)。

该断言表达式会返回 x 的值(也就是 value)和一个布尔值(也就是 ok),可根据该布尔值判断 x 是否为 T 类型:

  • 如果 T 是具体某个类型,类型断言会检查 x 的动态类型是否等于具体类型 T。如果检查成功,类型断言返回的结果是 x 的动态值,其类型是 T。
  • 如果 T 是接口类型,类型断言会检查 x 的动态类型是否满足 T。如果检查成功,x 的动态值不会被提取,返回值是一个类型为 T 的接口值。
  • 无论 T 是什么类型,如果 x 是 nil 接口值,类型断言都会失败。

示例代码:

package main
import (
    "fmt"
)
func main() {
    var x interface{}
    x = 10
    value, ok := x.(int)
    fmt.Print(value, ",", ok)
}

输出结果:

10,true

注意如果不接收第二个参数也就是上面代码中的 ok,断言失败时会直接造成一个 panic。如果 x 为 nil 同样也会 panic。

package main
import (
    "fmt"
)
func main() {
    var x interface{}
    x = "Hello"
    value := x.(int)
    fmt.Println(value)
}

输出结果:

panic: interface conversion: interface {} is string, not int

类型断言还可以配合 switch 使用,示例代码如下:

package main
import (
    "fmt"
)
func main() {
    var a int
    a = 10
    getType(a)
}
func getType(a interface{}) {
    switch a.(type) {
    case int:
        fmt.Println("the type of a is int")
    case string:
        fmt.Println("the type of a is string")
    case float64:
        fmt.Println("the type of a is float")
    default:
        fmt.Println("unknown type")
    }
}

输出结果:

the type of a is int

Map类型(map)

Map 是一种无序的键值对的集合。Map 最重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值。

Map 是一种集合,所以我们可以像迭代数组和切片那样迭代它。不过,Map 是无序的,我们无法决定它的返回顺序,这是因为 Map 是使用 hash 表来实现的。

定义 Map
可以使用内建函数 make 也可以使用 map 关键字来定义 Map:

/* 声明变量,默认 map 是 nil */
var map_variable map[key_data_type]value_data_type

/* 使用 make 函数 */
map_variable := make(map[key_data_type]value_data_type)

示例:创建一个普通的map集合。(但是map的真正使用不仅仅局限于示例。例如当一个key会有多个值的时候,我们可以将map的key声明为string类型,值是切片类型,那么该key就可以保存很多值)

package main

import "fmt"

func main() {
    var countryCapitalMap map[string]string /*创建集合 */
    countryCapitalMap = make(map[string]string)

    /* map插入key - value对,各个国家对应的首都 */
    countryCapitalMap [ "France" ] = "巴黎"
    countryCapitalMap [ "Italy" ] = "罗马"
    countryCapitalMap [ "Japan" ] = "东京"
    countryCapitalMap [ "India " ] = "新德里"

    /*使用键输出地图值 */
    for country := range countryCapitalMap {
        fmt.Println(country, "首都是", countryCapitalMap [country])
    }

    /*查看元素在集合中是否存在 */
    capital, ok := countryCapitalMap [ "American" ] /*如果确定是真实的,则存在,否则不存在 */
    /*fmt.Println(capital) */
    /*fmt.Println(ok) */
    if (ok) {
        fmt.Println("American 的首都是", capital)
    } else {
        fmt.Println("American 的首都不存在")
    }
}

输出结果:

France 首都是 巴黎
Italy 首都是 罗马
Japan 首都是 东京
India 首都是 新德里
American 的首都不存在

map容量
map 容量
和数组不同,map 可以根据新增的 key-value 动态的伸缩,因此它不存在固定长度或者最大限制,但是也可以选择标明 map 的初始容量 capacity,格式如下:

make(map[keytype]valuetype, cap)

#例如
map2 := make(map[string]float, 100)

当 map 增长到容量上限的时候,如果再增加新的 key-value,map 的大小会自动加 1,所以出于性能的考虑,对于大的 map 或者会快速扩张的 map,即使只是大概知道容量,也最好先标明。

参考链接

菜鸟教程
C语言中文网