go语言中不用写分号

Hello World

package main
//导入语句
import "fmt"
//函数外只能放置标识符的声明
//程序的入口函数
func main() {
    fmt.Println("hello,world!")
}

前言

在cmd中输入go version可以查看版本号

在项目目录下执行go build编译文件
在其他路径下后面加上项目的路径,路径从GOPATH\src后面写起

go build -o helloworld.exe 可以自己决定生成exe文件的名字

go run main.go执行,不推荐

跨平台编译

只需要指定目标操作系统的平台和处理器架构
SET CGO_ENABLED=0  // 禁用CGO
SET GOOS=linux  // 目标平台是linux
SET GOARCH=amd64  // 目标处理器架构是amd64

变量声明,go语言中变量必须先声明再使用

var 变量名称 变量类型

package main
import "fmt"
var s1 string
var num int
var s2 = "sss" //声明的同时赋值
//批量声明
var (
    name string //""
    age  int    //0
    sex  bool   //false
)
func main() {
    //变量的初始化
    name = "张三"
    age = 20
    sex = true
    //go语言中变量声明了必须使用,不然编译失败
    fmt.Print(sex)
    fmt.Printf("name:%s\n", name)
    fmt.Println("age:", age)
//简短变量声明,只能在函数内部使用
    s3 := "string"
    fmt.Println(s3)
    //匿名变量,用一个单独的下划线来表示,_
    //用于多重赋值忽略某个参数,如下有两个返回值只想取其中一个
    x, _ = function()
    _, y = function()
}

常量

//常量的定义
//定义了常量之后不能再修改了
const pi = 3.1415926535
const (
    n1 = "lisi"
    n2 //如果省略则与上一行相同
    a1 = 100
    a2
)

iota:ota是go语言的常量计数器,只能在常量的表达式中使用。

iota是go语言的常量计数器,只能在常量的表达式中使用。
iota在const关键字出现时将被重置为0。const中每新增一行常量声明将使iota计数一次(iota可理解为const语句块中的行索引)。 使用iota能简化定义,在定义枚举时很有用。

//使用_跳过某些值
const (
    n1 = iota //0
    n2        //1
    _
    n4        //3
)

//iota声明中间插队
const (
    n1 = iota //0
    n2 = 100  //100
    n3 = iota //2
    n4        //3
)
const n5 = iota //0

//多个iota定义在一行
const (
    a, b = iota + 1, iota + 2 //1,2
    c, d                      //2,3
    e, f                      //3,4
)

//定义数量级 (1左移10位,变成了10000000000,也就是十进制的1024。)
const (
    _  = iota             //0,不要
    KB = 1 << (10 * iota) //10000000000 ----- 1024
    MB = 1 << (10 * iota) //1 << 20     1左移20
    GB = 1 << (10 * iota)
    TB = 1 << (10 * iota)
    PB = 1 << (10 * iota)
)

基本数据类型

go语言中不能定义二进制数,需要定义只能强制转换

package main
var (
    n1 = 101    //十进制
    n2 = 077    //八进制,前面加0
    n3 = 0xff   //十六进制,前面加0x
)
func main() {
    fmt.Printf("十进制     n1:%d\n", n1) //十进制输出%d,101
    fmt.Printf("二进制     n1:%b\n", n1) //转为二进制%b,1100101
    fmt.Printf("八进制     n1:%o\n", n1) //转为八进制%o,145
    fmt.Printf("十六进制    n1:%x\n", n1) //转为十六进制%x,65
    fmt.Printf("十进制     n2:%d\n", n2) //63
    fmt.Printf("十进制     n3:%d\n", n3) //255
//声明一个uint8类型的变量
    n4 := uint8(20)
    fmt.Printf("n4:%T\n", n4) //查看变量的类型%T
}

浮点型:

默认go语言中小数都是float64类型的
不同类型不能直接赋值,float64与float32是完全不同的两个类型

复数:complex64和complex128

bool型:

bool类型默认值为false
不允许将整型强制转换为bool类型
bool类型不能参与运算

var value bool
fmt.Printf("value:%v\n", value) //%v是value的缩写,不管什么类型都能打印出来,打印出false,bool值默认为false

字符串操作

字符串是以双引号来包裹的,字符以单引号来包裹(单独的字母、汉字、符号表示一个字符)
一个字符 'A' = 1个字节
一个utf8的汉字 '我' = 一般占3个字节
UNcode编码一个汉字一般占2个字节

转义符

\r  回车符(返回行首)
    \n  换行符(直接跳到下一行的同列位置)
    \t  制表符
    \'  单引号
    \"  双引号
    \\  反斜杠

多行字符串

Go语言中要定义一个多行字符串时,就必须使用反引号字符:

//写在两个反引号之间的内容会原样输出
s1 := `第一行
    第二行
    第三行
    `
    fmt.Println(s1)

strings常用函数

len(str)	求长度
fmt.Println(len(ss4))                //len求长度

+或fmt.Sprintf 拼接字符串
ss3 := ss1 + ss2                     //用+拼接字符串
ss4 := fmt.Sprintf("%s%s", ss1, ss2) //用sprintf函数拼接

strings.Split 分割
fmt.Println(strings.Split(ss1, "m")) //分隔,将name以m分隔开来,输出结果:[na e:]


strings.Contains 判断是否包含
states := strings.Contains(ss1, "m") //ss1是否包含m,返回bool类型,输出结果:true

strings.HasPrefix,strings.HasSuffix 前缀/后缀判断
states = strings.HasPrefix(ss1, "n") //前缀判断
states = strings.HasSuffix(ss1, "n") //后缀判断

strings.Index(),strings.LastIndex() 子串出现的位置
num := strings.Index(ss1, "e") //返回子串出现的位置
num = strings.LastIndex(ss4, "a") //返回子串最后一次出现的位置

strings.Join(a[]string, sep string) join操作
fmt.Println(strings.Join(ss5, "A")) //实现了拼接的功能,用A将分隔开来的ss5连接起来

byte和rune类型

var a := '中'
var b := 'x'

Go 语言的字符有以下两种:
1. uint8类型,或者叫 byte 型,代表了ASCII码的一个字符。
2. rune类型,代表一个 UTF-8字符。
当需要处理中文、日文或者其他复合字符时,则需要用到rune类型。rune类型实际是一个int32。

遍历字符串两种方法

// 遍历字符串
func traversalString() {
    s := "hello你好"
    for i := 0; i < len(s); i++ { //byte
        fmt.Printf("%v(%c) ", s[i], s[i])
    }
    fmt.Println()
    for _, r := range s { //rune
        fmt.Printf("%v(%c) ", r, r)
    }
    fmt.Println()
}

修改字符串

要修改字符串,需要先将其转换成[]rune或[]byte,完成后再转换为string。无论哪种转换,都会重新分配内存,并复制字节数组。
因为字符串本身是不可修改的

func changeString() {
    s1 := "big"
    // 强制类型转换
    byteS1 := []byte(s1)
    byteS1[0] = 'p'
    fmt.Println(string(byteS1))
    s2 := "白萝卜"
    runeS2 := []rune(s2)
    runeS2[0] = '红'
    fmt.Println(string(runeS2))
}

类型转换

Go语言中只有强制类型转换,没有隐式类型转换。该语法只能在两个类型之间支持相互转换的时候使用。
强制类型转换的基本语法如下:

float64(a*a + b*b)

算数运算符

注意: ++(自增)和--(自减)在Go语言中是单独的语句,并不是运算符。

切片

切片的定义和初始化

//切片的定义和初始化
    var a []string
    var b = []int{1, 2, 3, 4, 5}
    fmt.Println(a)
    fmt.Println(b)
    //基于数组定义切片
    var intArray = [5]int{10, 20, 30, 40, 50}
    var intSlice = intArray[1:3] //左闭右开,左右省略分别表示0和切片操作数的长度
    //[:4]表示[0:4],  [2:]表示[2:len(intArray)]
    fmt.Println(intArray)
    fmt.Println(intSlice)
    A := [5]int{1, 2, 3, 4, 5}
    t := A[1:3:5] //切片的完整表示方法,a[low : high : max],切片的容量cap设置为max-low
    fmt.Println(A)
    fmt.Println(t)

使用make()函数构造切片

//构建长度为5,容量为10的int型切片,可以用len获取长度,用cap获取容量
    slice := make([]int, 5, 10) //注意构建的不为空, 因为给了长度是5, int默认填充00000
    fmt.Println(slice)

使用append()函数添加元素

//判断切片是否为空,使用len(slice) == 0
    if len(slice) == 5 {
        slice = append(slice, 100) //注意用原切片去接收,因为append是可以动态扩充的,容量不够时回自动扩充,在另一片地址重新构建一个切片
    }
    fmt.Println(slice) //append在后面追加元素[0 0 0 0 0 100]
    //切片的赋值拷贝,拷贝前后两个变量共享底层数组,对一个切片的修改会影响另一个切片的内容
    slice2 := slice //直接将slice赋值拷贝给slice2
    fmt.Println(slice) //[0 0 0 0 0 100]
    fmt.Println(slice2)
    slice2[0] = 50
    fmt.Println(slice) //两个都被同时修改了,[50 0 0 0 0 100]
    fmt.Println(slice2)

使用copy函数赋值切片,避免直接赋值共用一块内存的问题

//用copy函数可以避免这个问题,让两个分别对应两块不同的内存
    //var slice3 = []int{0, 0, 0, 0, 0, 0}
    slice3 := make([]int, 6, 10)
    copy(slice3, slice)
    slice3[1] = 66
    fmt.Println(slice)  //[50 0 0 0 0 100]
    fmt.Println(slice3) //[50 66 0 0 0 100] 不会同时修改

切片删除元素,利用append函数

  //本身没有删除元素的函数,可以利用append函数实现
    slice4 := make([]int, 6, 10)
    copy(slice4, slice3)
    fmt.Println(slice4)                        //[50 66 0 0 0 100]
    slice4 = append(slice4[:0], slice4[1:]...) //必须要加上...
    fmt.Println(slice4)                        //[66 0 0 0 100] 删除了第0号元素
    //总结:要从切片a中删除索引为index的元素,操作方法是a = append(a[:index], a[index+1:]...)

判断切片是否为空

// 要检查切片是否为空,请始终使用len(s) == 0来判断,而不应该使用s == nil来判断。
nil代表切片的0值,只有在切片声明但并未初始化,才有slice==nil
var slice []int         //len(s1)=0;cap(s1)=0;s1==nil

切片不能直接比较

// 切片之间是不能比较的,我们不能使用==操作符来判断两个切片是否含有全部相等元素。 切片唯一合法的比较操作是和nil比较。 一个nil值的切片并没有底层数组,一个nil值的切片的长度和容量都是0。但是我们不能说一个长度和容量都是0的切片一定是nil,例如下面的示例:
var s1 []int         //len(s1)=0;cap(s1)=0;s1==nil
s2 := []int{}        //len(s2)=0;cap(s2)=0;s2!=nil
s3 := make([]int, 0) //len(s3)=0;cap(s3)=0;s3!=nil

map

map的定义

//map的定义,map里的元素都是成对的
    scoreMap := make(map[string]int, 10) //初始化并设置容量
//map的初始化,map里的元素都是成对的
    scoreMap["张三"] = 90
    scoreMap["李四"] = 100
    scoreMap["王五"] = 60

用for range的遍历方法遍历map

for k, v := range scoreMap { 
        fmt.Println(k)
        fmt.Println(v)
    }
for k := range scoreMap { //只需要遍历key时,可以省略对应value的_
        fmt.Println(k) //仅限遍历key, 如果省略不写默认接收key
    }
    for _, value := range scoreMap { //遍历value时需要接收两个
        fmt.Println(value)
    }

判断某个键是否存在

   value, ok := scoreMap["张三"] //判断是否有key:张三的存在,存在ok为true并将key对应的值传入value中,
    if ok {                     //否则为false并传入value类型的零值
        fmt.Println(value)
    } else {
        fmt.Println("error")
    }

使用delete删除键值对

delete(scoreMap, "娜扎") //删除key为娜扎的键值对
    fmt.Println(scoreMap)

在声明的同时填充元素

 userInfo := map[string]string{
        "username""plf",
        "password""123456",
    }
    fmt.Println(userInfo)

随机数的使用方法

  rand.Seed(time.Now().UnixNano()) //初始化随机数种子
    for i := 0; i < 5; i++ {
        num := rand.Intn(100) //生成0~99的随机整数
        fmt.Println(num)
    }

函数

//指针
func swap(x *int, y *int) {
    temp := *x
    *x = *y
    *y = temp
}
swap(&x, &y)
//没有引用的用法 error
// func swap2(x &int, y &int) {
//  temp := x
//  x = y
//  y = temp
// }
func sum(x, y int) int {
    return x + y
}

//go语言中没有默认参数
//可以同时接收固定参数和可变参数, 可变参数要放在最后
//形参接收可变参数,字后面加..., 参数是个切片类型
func sumAny(a ...int) int {
    result := 0
    for _, value := range a { //go中默认接收两个值, 下标和值, 只写一个默认接收下标, 在map中接收的则是key
        result += value
    }
    return result
}

defer

//在defer归属的函数即将返回时,先被defer的语句最后被执行,最后被defer的语句,最先被执行。
    fmt.Println(10)
    defer fmt.Println(1) 
    defer fmt.Println(2)
    defer fmt.Println(3)
    fmt.Println(20)
//执行顺序 10, 20, 3, 2, 1

闭包

func myFunc(name string) func() {
    //外层变量(形参中)
    return func() { //返回了一个匿名函数
        fmt.Println("hello", name) //引用了外层变量
    }
}
//闭包 = 函数 + 外层变量的引用
ret := myFunc("plf") //ret就是一个闭包
ret()           //执行了返回的匿名函数

闭包实例

//加后缀的函数
func addSuffix(suffix string) func(string) string {
    return func(name string) string {
        if !strings.HasSuffix(name, suffix) { //如果后缀不是suffix就为name加上suffix,否则返回name
            return name + suffix
        }
        return name
    }
}
res := addSuffix(".avi") //输入suffix的值, 并返回匿名函数, 此时res为一个闭包 
temp := res("nihao")     //输入name的值, 并返回加后缀后的结果
fmt.Println(temp)        //打印结果

panir/recover异常处理

//recover()必须搭配defer使用。defer一定要在可能引发panic的语句之前定义。
func b() {
    defer func() { //必须写在发生异常的前面
        err := recover() //接收错误信息
        if err != nil {  //不为空则说明发生了异常
            fmt.Println("b error")
        }
    }()
    panic("panic b") //报异常
}

指针

//&取出地址,*根据地址取出地址指向的值
func swap(x, y *int) {
    temp := *x
    *x = *y
    *y = temp
}
swap(&x, &y)
fmt.Println(x, y

new开辟内存

    p := new(int) //new开辟内存, 并把对应内存的地址返回
    fmt.Println(p)
    fmt.Println(*p)
    *p = 999
    fmt.Println(p)
    fmt.Println(*p)


//new用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针。
//而make只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身;

自定义类型与取别名

//自定义类型

//MyInt the type of int
type MyInt int //要写上注释, 否则会警告, 名字是大写的时候

//类型别名

//Byte the type of uint8
type Byte = uint8

var x MyInt
fmt.Printf("%T, %d\n", x, x) //main.MyInt, 0
var y Byte
fmt.Printf("%T, %d\n", y, y) //uint8, 0

结构体

结构体定义

//结构体定义
type person struct {
    name string
    age  int
    sex  string
}

结构体的初始化

var p1 person
    p1.name = "zhangsan"
    p1.age = 20
    p1.sex = "man"

使用键值初始化

    p2 := person{ //注意每一行后面有逗号
        name: "lisi",
        age:  18,
        sex:  "woman",
    }

//简写,初始化的时候不写键,注意:
    // 必须初始化结构体的所有字段。
    // 初始值的填充顺序必须与字段在结构体中的声明顺序一致。
    // 该方式不能和键值初始化方式混用。
    p3 := &person{ //注意每一行后面有逗号
        "wangwu",
        18,
        "woman",
    }

匿名结构体

    var user struct {
        name     string
        password int
    }
    user.name = "plf"
    user.password = 123456
    fmt.Println(user)

结构体字段的可见性

结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)。

构造函数

func newPerson(name, city string, age int8) *person {
    return &person{
        name: name,
        city: city,
        age:  age,
    }
}

方法和接收者

//方法的定义格式如下:
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
    函数体
}

//例
//Person 结构体
type Person struct {
    name string
    age  int8
}
//NewPerson 构造函数
func NewPerson(name string, age int8) *Person {
    return &Person{
        name: name,
        age:  age,
    }
}
//Dream Person做梦的方法
func (p Person) Dream() {
    fmt.Printf("%s的梦想是学好Go语言!\n", p.name)
}
func main() {
    p1 := NewPerson("plf", 18)
    p1.Dream()
}

任意类型添加方法

在Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。 举个例子,我们基于内置的int类型使用type关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法。
注意事项: 非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法。

定义包

package 包名

可见性 对外可见的成员注意要写注释

如果想在一个包中引用另外一个包里的标识符(如变量、常量、类型、函数等)时,该标识符必须是对外可见的(public)。
在Go语言中只需要将标识符的首字母大写就可以让标识符对外可见了。
// 包变量可见性
var a = 100 // 首字母小写,外部包不可见,只能在当前包内使用
// 首字母大写外部包可见,可在其他包中使用
const Mode = 1
type person struct { // 首字母小写,外部包不可见,只能在当前包内使用
    name string
}
// Add 首字母大写,外部包可见,可在其他包中使用
func Add(x, y int) int {
    return x + y
}
func age() { // 首字母小写,外部包不可见,只能在当前包内使用
    var Age = 18 // 函数局部变量,外部包不可见,只能在当前函数内使用
    fmt.Println(Age)
}


//结构体中的字段名和接口中的方法名如果首字母都是大写,外部包可以访问这些字段和方法
type Student struct {
    Name  string //可在包外访问的方法
    class string //仅限包内访问的字段
}
type Payer interface {
    init() //仅限包内访问的方法
    Pay()  //可在包外访问的方法
}


包的导入

要在代码中引用其他包的内容,需要使用import关键字导入使用的包。具体语法如下:
import "包的路径"
注意事项:
import导入语句通常放在文件开头包声明语句的下面。
导入的包名需要使用双引号包裹起来。
包名是从$GOPATH/src/后开始计算的,使用/进行路径分隔。
Go语言中禁止循环导入包。

//单行导入
单行导入的格式如下:
import "包1"
import "包2"
//多行导入
多行导入的格式如下:
import (
    "包1"
    "包2"
)


自定义包名

在导入包名的时候,我们还可以为导入的包设置别名。通常用于导入的包名太长或者导入的包名冲突的情况。具体语法格式如下:
import 别名 "包的路径"
单行导入方式定义别名:
import "fmt"
import m "github.com/Q1mi/studygo/pkg_test"
func main() {
    fmt.Println(m.Add(100, 200))
    fmt.Println(m.Mode)
}
多行导入方式定义别名:
import (
    "fmt"
    m "github.com/Q1mi/studygo/pkg_test"
 )

匿名导入包

如果只希望导入包,而不使用包内部的数据时,可以使用匿名导入包。具体的格式如下:
import _ "包的路径"
匿名导入的包与其他方式导入的包一样都会被编译到可执行文件中。

init()初始化函数

init()函数介绍
在Go语言程序执行时导入包语句会自动触发包内部init()函数的调用。
需要注意的是: init()函数没有参数也没有返回值。
init()函数在程序运行时自动被调用执行,不能在代码中主动调用它。

init函数的执行时机:在全局声明之后,main函数之前
全局声明-> init函数 -> main函数

init()函数执行顺序

Go语言包会从main包开始检查其导入的所有包,每个包中又可能导入了其他的包。Go编译器由此构建出一个树状的包引用关系,再根据引用顺序决定编译顺序,依次编译这些包的代码。
在运行时,被最后导入的包会最先初始化并调用其init()函数

重写init函数,相当于构造函数


func init() {
    fmt.Println("haha")
}

接口

接口是一种抽象的类型

在Go语言中接口(interface)是一种类型,一种抽象的类型。
interface是一组method的集合,是duck-type programming的一种体现。接口做的事情就像是定义一个协议(规则),只要一台机器有洗衣服和甩干的功能,我就称它为洗衣机。不关心属性(数据),只关心行为(方法)。
为了保护你的Go语言职业生涯,请牢记接口(interface)是一种类型。

接口的定义

每个接口由数个方法组成,接口的定义格式如下:
type 接口类型名 interface{
    方法名1( 参数列表1 ) 返回值列表1
    方法名2( 参数列表2 ) 返回值列表2
    …
}
接口名:使用type将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加er,如有写操作的接口叫Writer,有字符串功能的接口叫Stringer等。接口名最好要能突出该接口的类型含义。
方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。

示例

package main
import "fmt"
//狗
type dog struct{}
func (d dog) say() {
    fmt.Println("www~")
}
//猫
type cat struct{}
func (c catsay() {
    fmt.Println("mmm~")
}
//人
type person struct {
    name string
}
func (p person) say() {
    fmt.Println("aaa~")
}
//接口,不管你是什么类型,只管你的方法
//定义了一个抽象的类型, 只要你实现了say这个方法,都可以称为sayer这个类型
//一个对象只要全部实现了接口中的方法,那么就实现了这个接口。换句话说,接口就是一个需要实现的方法列表。
//一个接口类型的变量可以存储所有实现了这个接口的变量
type sayer interface {
    say()
}
//sayer代表方法,而不指定类型
func hit(arg sayer) {
    arg.say() //不管传什么进来都执行say()方法
}
func main() {
    var d dog
    hit(d)
    var c cat
    hit(c)
    p := person{
        name: "张三",
    }
    hit(p)
    var s sayer //接口类型的变量
    s = p       //一个接口类型的变量可以存储所有实现了这个接口的变量
    fmt.Println(s)
    s.say() //可以直接调用
    hit(s)  //也可以作为形参传入
}

值接收者和指针接收者实现接口的区别

package main
import "fmt"
//定义了一个接口
type mover interface {
    move()
}
//结构体
type person struct {
    name string
    age  int
}
//实现了接口里的所有方法,使用值类型接收
//使用值类型接收时,m可以接收值类型与指针类型
func (p person) move() {
    fmt.Printf("%s is moving\n", p.name)
}
//实现了接口里的所有方法,使用指针类型接收
//使用指针类型接收时,m只能接收指针类型
// func (p *person) move() {
//  fmt.Printf("%s is moving\n", p.name)
// }
func main() {
    var m mover
    p1 := person{
        name: "zhangsan",
        age:  20,
    }
    p2 := &person{
        name: "lisi",
        age:  18,
    }
    m = p1
    m = p2
    fmt.Println(m)
    m.move()
}

接口嵌套

接口与接口间可以通过嵌套创造出新的接口。
// Sayer 接口
type Sayer interface {
    say()
}
// Mover 接口
type Mover interface {
    move()
}
// 接口嵌套
type animal interface {
    Sayer
    Mover
}


空接口

空接口的定义
空接口是指没有定义任何方法的接口。因此任何类型都实现了空接口。
空接口类型的变量可以存储任意类型的变量。
func main() {
    // 定义一个空接口x
    var x interface{}
    s := "Hello"
    x = s
    fmt.Printf("type:%T value:%v\n", x, x)
    i := 100
    x = i
    fmt.Printf("type:%T value:%v\n", x, x)
    b := true
    x = b
    fmt.Printf("type:%T value:%v\n", x, x)
}

空接口的应用

空接口作为函数的参数
使用空接口实现可以接收任意类型的函数参数。
// 空接口作为函数参数
func show(a interface{}) {
    fmt.Printf("type:%T value:%v\n", a, a)
}
空接口作为map的值
使用空接口实现可以保存任意类型值的字典。
// 空接口作为map值
    var studentInfo = make(map[string]interface{})
    studentInfo["name"] = "张三"
    studentInfo["age"] = 18
    studentInfo["married"] = false
    fmt.Println(studentInfo)

类型断言

想要判断空接口中的值这个时候就可以使用类型断言,其语法格式:
x.(T)
其中:
x:表示类型为interface{}的变量
T:表示断言x可能是的类型。
该语法返回两个参数,第一个参数是x转化为T类型后的变量,第二个值是一个布尔值,若为true则表示断言成功,为false则表示断言失败。
例:
func main() {
    var x interface{}
    x = "Hello"
    v, ok := x.(string) //v储存值,ok判断x是否为string类型,是则ok返回true,否则false
    if ok {
        fmt.Println(v)
    } else {
        fmt.Println("类型断言失败")
    }
}

反射

reflect包

在Go语言的反射机制中,任何接口值都由是一个具体类型和具体类型的值两部分组成的(我们在上一篇接口的博客中有介绍相关概念)。
在Go语言中反射的相关功能由内置的reflect包提供,任意接口值在反射中都可以理解为由reflect.Type和reflect.Value两部分组成,
并且reflect包提供了reflect.TypeOf和reflect.ValueOf两个函数来获取任意对象的Value和Type。

TypeOf

在Go语言中,使用reflect.TypeOf()函数可以获得任意值的类型对象(reflect.Type),程序通过类型对象可以访问任意值的类型信息。
package main
import (
    "fmt"
    "reflect"
)
func reflectType(x interface{}) {
    v := reflect.TypeOf(x)
    fmt.Printf("type:%v\n", v)
}
func main() {
    var a float32 = 3.14
    reflectType(a) // type:float32
    var b int64 = 100
    reflectType(b) // type:int64
}

type name和type kind

在反射中关于类型还划分为两种:类型(Type)和种类(Kind)。因为在Go语言中我们可以使用type关键字构造很多自定义类型,而种类(Kind)就是指底层的类型,但在反射中,当需要区分指针、结构体等大品种的类型时,就会用到种类(Kind)。 举个例子,我们定义了两个指针类型和两个结构体类型,通过反射查看它们的类型和种类。
package main
import (
    "fmt"
    "reflect"
)
type myInt int64
func reflectType(x interface{}) {
    t := reflect.TypeOf(x)
    fmt.Printf("type:%v kind:%v\n", t.Name(), t.Kind())
}
func main() {
    var a *float32 // 指针
    var b myInt    // 自定义类型
    var c rune     // 类型别名
    reflectType(a) // type: kind:ptr
    reflectType(b) // type:myInt kind:int64
    reflectType(c) // type:int32 kind:int32
    type person struct {
        name string
        age  int
    }
    type book struct{ title string }
    var d = person{
        name: "张三",
        age:  18,
    }
    var e = book{title: "Go语言"}
    reflectType(d) // type:person kind:struct
    reflectType(e) // type:book kind:struct
}
Go语言的反射中像数组、切片、Map、指针等类型的变量,它们的.Name()都是返回空。

ValueOf

reflect.ValueOf()返回的是reflect.Value类型,其中包含了原始值的值信息。reflect.Value与原始值之间可以互相转换。
通过反射获取值
func reflectValue(x interface{}) {
    v := reflect.ValueOf(x)
    k := v.Kind()
    switch k {
    case reflect.Int64:
        // v.Int()从反射中获取整型的原始值,然后通过int64()强制类型转换
        fmt.Printf("type is int64, value is %d\n", int64(v.Int()))
    case reflect.Float32:
        // v.Float()从反射中获取浮点型的原始值,然后通过float32()强制类型转换
        fmt.Printf("type is float32, value is %f\n", float32(v.Float()))
    case reflect.Float64:
        // v.Float()从反射中获取浮点型的原始值,然后通过float64()强制类型转换
        fmt.Printf("type is float64, value is %f\n", float64(v.Float()))
    }
}
func main() {
    var a float32 = 3.14
    var b int64 = 100
    reflectValue(a) // type is float32, value is 3.140000
    reflectValue(b) // type is int64, value is 100
    // 将int类型的原始值转换为reflect.Value类型
    c := reflect.ValueOf(10)
    fmt.Printf("type c :%T\n", c) // type c :reflect.Value
}

通过反射设置变量的值

想要在函数中通过反射修改变量的值,需要注意函数参数传递的是值拷贝,必须传递变量地址才能修改变量值。而反射中使用专有的Elem()方法来获取指针对应的值。
package main
import (
    "fmt"
    "reflect"
)
func reflectSetValue1(x interface{}) {
    v := reflect.ValueOf(x)
    if v.Kind() == reflect.Int64 {
        v.SetInt(200) //修改的是副本,reflect包会引发panic
    }
}
func reflectSetValue2(x interface{}) {
    v := reflect.ValueOf(x)
    // 反射中使用 Elem()方法获取指针对应的值
    if v.Elem().Kind() == reflect.Int64 {
        v.Elem().SetInt(200)
    }
}
func main() {
    var a int64 = 100
    // reflectSetValue1(a) //panic: reflect: reflect.Value.SetInt using unaddressable value
    reflectSetValue2(&a)
    fmt.Println(a)
}
isNil()和isValid()
IsNil()常被用于判断指针是否为空;IsValid()常被用于判定返回值是否有效。
func main() {
    // *int类型空指针
    var a *int
    fmt.Println("var a *int IsNil:", reflect.ValueOf(a).IsNil())
    // nil值
    fmt.Println("nil IsValid:", reflect.ValueOf(nil).IsValid())
    // 实例化一个匿名结构体
    b := struct{}{}
    // 尝试从结构体中查找"abc"字段
    fmt.Println("不存在的结构体成员:", reflect.ValueOf(b).FieldByName("abc").IsValid())
    // 尝试从结构体中查找"abc"方法
    fmt.Println("不存在的结构体方法:", reflect.ValueOf(b).MethodByName("abc").IsValid())
    // map
    c := map[string]int{}
    // 尝试从map中查找一个不存在的键
    fmt.Println("map中不存在的键:", reflect.ValueOf(c).MapIndex(reflect.ValueOf("娜扎")).IsValid())
}


结构体反射

StructField类型
StructField类型用来描述结构体中的一个字段的信息。
结构体反射示例
当我们使用反射得到一个结构体数据之后可以通过索引依次获取其字段信息,也可以通过字段名去获取指定的字段信息。
type student struct {
    Name  string `json:"name"`
    Score int    `json:"score"`
}
func main() {
    stu1 := student{
        Name:  "zhansan",
        Score: 90,
    }
    t := reflect.TypeOf(stu1)
    fmt.Println(t.Name(), t.Kind()) // student struct
    // 通过for循环遍历结构体的所有字段信息
    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        fmt.Printf("name:%s index:%d type:%v json tag:%v\n", field.Name, field.Index, field.Type, field.Tag.Get("json"))
    }
    // 通过字段名获取指定结构体字段信息
    if scoreField, ok := t.FieldByName("Score"); ok {
        fmt.Printf("name:%s index:%d type:%v json tag:%v\n", scoreField.Name, scoreField.Index, scoreField.Type, scoreField.Tag.Get("json"))
    }
}

编写函数printMethod(s interface{})来遍历打印s包含的方法。

// 给student添加两个方法 Study和Sleep(注意首字母大写)
func (s student) Study() string {
    msg := "好好学习,天天向上。"
    fmt.Println(msg)
    return msg
}
func (s student) Sleep() string {
    msg := "好好睡觉,快快长大。"
    fmt.Println(msg)
    return msg
}
func printMethod(x interface{}) {
    t := reflect.TypeOf(x)
    v := reflect.ValueOf(x)
    fmt.Println(t.NumMethod())
    for i := 0; i < v.NumMethod(); i++ {
        methodType := v.Method(i).Type()
        fmt.Printf("method name:%s\n", t.Method(i).Name)
        fmt.Printf("method:%s\n", methodType)
        // 通过反射调用方法传递的参数必须是 []reflect.Value 类型
        var args = []reflect.Value{}
        v.Method(i).Call(args)
    }
}

并发

goroutine

package main
import (
    "fmt"
    "sync"
)
var wg sync.WaitGroup
func hello() {
    fmt.Println("hello world")
    wg.Done() //通知计数器减1
}
func main() { //开启一个主goroutine去执行main函数
    wg.Add(1)  //计数器加1
    go hello() //另开启一个goroutine去执行hello函数
    fmt.Println("nihao")
    // time.Sleep(time.Second) //等待1秒
    wg.Wait() //阻塞住, 直到计数器变为0, 才退出
}

GOMAXPROCS

Go运行时的调度器使用GOMAXPROCS参数来确定需要使用多少个OS线程来同时执行Go代码。默认值是机器上的CPU核心数。例如在一个8核心的机器上,调度器会把Go代码同时调度到8个OS线程上(GOMAXPROCS是m:n调度中的n)。
Go语言中可以通过runtime.GOMAXPROCS()函数设置当前程序并发时占用的CPU逻辑核心数。
Go1.5版本之前,默认使用的是单核心执行。Go1.5版本之后,默认使用全部的CPU逻辑核心数。
我们可以通过将任务分配到不同的CPU逻辑核心上实现并行的效果,这里举个例子:
func a() {
    for i := 1; i < 10; i++ {
        fmt.Println("A:", i)
    }
}
func b() {
    for i := 1; i < 10; i++ {
        fmt.Println("B:", i)
    }
}
func main() {
    runtime.GOMAXPROCS(1) //指定为1个核心
    go a()
    go b()
    time.Sleep(time.Second)
}


channel类型

定义

channel是一种类型,一种引用类型。声明通道类型的格式如下:
var 变量 chan 元素类型
举几个例子:
var ch1 chan int   // 声明一个传递整型的通道
var ch2 chan bool  // 声明一个传递布尔型的通道
var ch3 chan []int // 声明一个传递int切片的通道

创建channel

通道是引用类型,通道类型的空值是nil。
var ch chan int
fmt.Println(ch) // <nil>
声明的通道后需要使用make函数初始化之后才能使用。
创建channel的格式如下:
make(chan 元素类型, [缓冲大小])
channel的缓冲大小是可选的。
举几个例子:
ch4 := make(chan int)
ch5 := make(chan bool)
ch6 := make(chan []int)

channel操作

通道有发送(send)、接收(receive)和关闭(close)三种操作。
发送和接收都使用<-符号。
现在我们先使用以下语句定义一个通道:
ch := make(chan int)

发送

将一个值发送到通道中。
ch <- 10 // 把10发送到ch中

接收

从一个通道中接收值。
x := <- ch // 从ch中接收值并赋值给变量x
<-ch       // 从ch中接收值,忽略结果

关闭

我们通过调用内置的close函数来关闭通道。
close(ch)
关于关闭通道需要注意的事情是,只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。
关闭后的通道有以下特点:
对一个关闭的通道再发送值就会导致panic。
对一个关闭的通道进行接收会一直获取值直到通道为空。
对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
关闭一个已经关闭的通道会导致panic。

无缓冲的通道

无缓冲的通道又称为阻塞的通道。我们来看一下下面的代码:
func main() {
    ch := make(chan int)
    ch <- 10
    fmt.Println("发送成功")
}
上面这段代码能够通过编译,但是执行的时候会出现以下错误:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
        .../src/github.com/Q1mi/studygo/day06/channel02/main.go:8 +0x54
为什么会出现deadlock错误呢?
因为我们使用ch := make(chan int)创建的是无缓冲的通道,无缓冲的通道只有在有人接收值的时候才能发送值。就像你住的小区没有快递柜和代收点,快递员给你打电话必须要把这个物品送到你的手中,简单来说就是无缓冲的通道必须有接收才能发送。
上面的代码会阻塞在ch <- 10这一行代码形成死锁,那如何解决这个问题呢?
一种方法是启用一个goroutine去接收值,例如:
func recv(c chan int) {
    ret := <-c
    fmt.Println("接收成功", ret)
}
func main() {
    ch := make(chan int)
    go recv(ch) // 启用goroutine从通道接收值
    ch <- 10
    fmt.Println("发送成功")
}
无缓冲通道上的发送操作会阻塞,直到另一个goroutine在该通道上执行接收操作,这时值才能发送成功,两个goroutine将继续执行。相反,如果接收操作先执行,接收方的goroutine将阻塞,直到另一个goroutine在该通道上发送一个值。
使用无缓冲通道进行通信将导致发送和接收的goroutine同步化。因此,无缓冲通道也被称为同步通道。

有缓冲的通道

解决上面问题的方法还有一种就是使用有缓冲区的通道。我们可以在使用make函数初始化通道的时候为其指定通道的容量,例如:
func main() {
    ch := make(chan int, 1) // 创建一个容量为1的有缓冲区通道
    ch <- 10
    fmt.Println("发送成功")
}
只要通道的容量大于零,那么该通道就是有缓冲的通道,通道的容量表示通道中能存放元素的数量。就像你小区的快递柜只有那么个多格子,格子满了就装不下了,就阻塞了,等到别人取走一个快递员就能往里面放一个。
我们可以使用内置的len函数获取通道内元素的数量,使用cap函数获取通道的容量,虽然我们很少会这么做。


for range从通道循环取值

当向通道中发送完数据时,我们可以通过close函数来关闭通道。
当通道被关闭时,再往该通道发送值会引发panic,从该通道取值的操作会先取完通道中的值,再然后取到的值一直都是对应类型的零值。那如何判断一个通道是否被关闭了呢?
我们来看下面这个例子:
// channel 练习
func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)
    // 开启goroutine将0~100的数发送到ch1中
    go func() {
        for i := 0; i < 100; i++ {
            ch1 <- i
        }
        close(ch1)
    }()
    // 开启goroutine从ch1中接收值,并将该值的平方发送到ch2中
    go func() {
        for {
            i, ok := <-ch1 // 通道关闭后再取值ok=false
            if !ok {
                break
            }
            ch2 <- i * i
        }
        close(ch2)
    }()
    // 在主goroutine中从ch2中接收值打印
    for i := range ch2 { // 通道关闭后会退出for range循环
        fmt.Println(i)
    }
}
从上面的例子中我们看到有两种方式在接收值的时候判断该通道是否被关闭,不过我们通常使用的是for range的方式。
使用for range遍历通道,当通道被关闭的时候就会退出for range。

单向通道

有的时候我们会将通道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用通道都会对其进行限制,比如限制通道在函数中只能发送或只能接收。
Go语言中提供了单向通道来处理这种情况。例如,我们把上面的例子改造如下:
func counter(out chan<- int) {
    for i := 0; i < 100; i++ {
        out <- i
    }
    close(out)
}
func squarer(out chan<- int, in <-chan int) {
    for i := range in {
        out <- i * i
    }
    close(out)
}
func printer(in <-chan int) {
    for i := range in {
        fmt.Println(i)
    }
}
func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)
    go counter(ch1)
    go squarer(ch2, ch1)
    printer(ch2)
}
其中,
chan<- int是一个只写单向通道(只能对其写入int类型值),可以对其执行发送操作但是不能执行接收操作;
<-chan int是一个只读单向通道(只能从其读取int类型值),可以对其执行接收操作但是不能执行发送操作。
在函数传参及任何赋值操作中可以将双向通道转换为单向通道,但反过来是不可以的。

worker pool(goroutine池)

在工作中我们通常会使用可以指定启动的goroutine数量–worker pool模式,控制goroutine的数量,防止goroutine泄漏和暴涨。
一个简易的work pool示例代码如下:

package main
import (
    "fmt"
    "time"
)
func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        fmt.Printf("worker:%d start job:%d\n"id, j)
        time.Sleep(time.Millisecond * 500)
        fmt.Printf("worker:%d end job:%d\n"id, j)
        results <- j * 2
    }
}
func main() {
    jobs := make(chan int, 100)
    results := make(chan int, 100)
    // 开启3个goroutine
    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }
    // 5个任务
    for j := 1; j <= 5; j++ {
        jobs <- j
    }
    close(jobs)
    // 输出结果
    for a := 1; a <= 5; a++ {
        <-results
    }
}

select多路复用

select的使用类似于switch语句,它有一系列case分支和一个默认的分支。每个case会对应一个通道的通信(接收或发送)过程。select会一直等待,直到某个case的通信操作完成时,就会执行case分支对应的s语句。具体格式如下:
select{
    case <-ch1:
        ...
    case data := <-ch2:
        ...
    case ch3<-data:
        ...
    default:
        默认操作
}
举个小例子来演示下select的使用:
func main() {
    ch := make(chan int, 1)
    for i := 0; i < 10; i++ {
        select {
        case x := <-ch:
            fmt.Println(x)
        case ch <- i:
        }
    }
}
使用select语句能提高代码的可读性。
可处理一个或多个channel的发送/接收操作。
如果多个case同时满足,select会随机选择一个。
对于没有case的select{}会一直等待,可用于阻塞main函数。

并发安全和锁

有时候在Go代码中可能会存在多个goroutine同时操作一个资源(临界区),这种情况会发生竞态问题(数据竞态)。
类比现实生活中的例子有十字路口被各个方向的的汽车竞争;还有火车上的卫生间被车厢里的人竞争。

互斥锁

互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个goroutine可以访问共享资源。
Go语言中使用sync包的Mutex类型来实现互斥锁。
package main
import (
    "fmt"
    "sync"
)
var (
    x    int64
    wg   sync.WaitGroup
    lock sync.Mutex //互斥锁
)
func add() {
    for i := 0; i < 5000; i++ {
        lock.Lock() //上锁
        x++
        lock.Unlock() //解锁
    }
    wg.Done()
}
func main() {
    wg.Add(2)
    go add()
    go add()
    wg.Wait()
    fmt.Println(x)
}

读写互斥锁

互斥锁是完全互斥的,但是有很多实际的场景下是读多写少的,当我们并发的去读取一个资源不涉及资源修改的时候是没有必要加锁的,这种场景下使用读写锁是更好的一种选择。读写锁在Go语言中使用sync包中的RWMutex类型。
读写锁分为两种:读锁和写锁。当一个goroutine获取读锁之后,其他的goroutine如果是获取读锁会继续获得锁,如果是获取写锁就会等待;当一个goroutine获取写锁之后,其他的goroutine无论是获取读锁还是写锁都会等待。

package main
import (
    "fmt"
    "sync"
    "time"
)
var (
    x      int64
    wg     sync.WaitGroup
    lock   sync.Mutex   //用互斥锁用时1.88s
    rwlock sync.RWMutex //用读写锁用时111ms
)
func read() {
    // lock.Lock()
    rwlock.RLock()
    time.Sleep(time.Millisecond)
    // lock.Unlock()
    rwlock.RUnlock()
    wg.Done()
}
func write() {
    // lock.Lock()
    rwlock.Lock()
    x++
    time.Sleep(time.Millisecond * 10)
    // lock.Unlock()
    rwlock.Unlock()
    wg.Done()
}
func main() {
    start := time.Now()
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go read()
    }
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go write()
    }
    wg.Wait()
    fmt.Println(time.Now().Sub(start))
}

sync.Once

在编程的很多场景下我们需要确保某些操作在高并发的场景下只执行一次,例如只加载一次配置文件、只关闭一次通道等。
Go语言中的sync包中提供了一个针对只执行一次场景的解决方案–sync.Once。
sync.Once只有一个Do方法,其签名如下:
func (o *Once) Do(f func()) {}
备注:如果要执行的函数f需要传递参数就需要搭配闭包来使用。
加载配置文件示例
var icons map[string]image.Image
var loadIconsOnce sync.Once
func loadIcons() {
    icons = map[string]image.Image{
        "left":  loadIcon("left.png"),
        "up":    loadIcon("up.png"),
        "right": loadIcon("right.png"),
        "down":  loadIcon("down.png"),
    }
}
// Icon 是并发安全的
func Icon(name string) image.Image {
    loadIconsOnce.Do(loadIcons)
    return icons[name]
}

sync.Map

Go语言的sync包中提供了一个开箱即用的并发安全版map–sync.Map。开箱即用表示不用像内置的map一样使用make函数初始化就能直接使用。同时sync.Map内置了诸如Store、Load、LoadOrStore、Delete、Range等操作方法。
var m = sync.Map{}
func main() {
    wg := sync.WaitGroup{}
    for i := 0; i < 20; i++ {
        wg.Add(1)
        go func(n int) {
            key := strconv.Itoa(n)
            m.Store(key, n)
            value, _ := m.Load(key)
            fmt.Printf("k=:%v,v:=%v\n", key, value)
            wg.Done()
        }(i)
    }
    wg.Wait()
}

原子操作

原子操作:使用内置的函数对数据进行操作

代码中的加锁操作因为涉及内核态的上下文切换会比较耗时、代价比较高。针对基本数据类型我们还可以使用原子操作来保证并发安全,因为原子操作是Go语言提供的方法它在用户态就可以完成,因此性能比加锁操作更好。Go语言中原子操作由内置的标准库sync/atomic提供。
atomic包
package main
import (
    "fmt"
    "sync"
    "sync/atomic"
    "time"
)
//Counter interface
type Counter interface {
    Inc()
    Load() int64
}
// CommonCounter 普通版
type CommonCounter struct {
    counter int64
}
//Inc inc
func (c CommonCounter) Inc() {
    c.counter++
}
//Load load
func (c CommonCounter) Load() int64 {
    return c.counter
}
//MutexCounter 互斥锁版
type MutexCounter struct {
    counter int64
    lock    sync.Mutex
}
//Inc inc
func (m *MutexCounter) Inc() {
    m.lock.Lock()
    defer m.lock.Unlock()
    m.counter++
}
//Load load
func (m *MutexCounter) Load() int64 {
    m.lock.Lock()
    defer m.lock.Unlock()
    return m.counter
}
//AtomicCounter 原子操作版
type AtomicCounter struct {
    counter int64
}
//Inc inc
func (a *AtomicCounter) Inc() {
    atomic.AddInt64(&a.counter, 1)
}
//Load load
func (a *AtomicCounter) Load() int64 {
    return atomic.LoadInt64(&a.counter)
}
func test(c Counter) {
    var wg sync.WaitGroup
    start := time.Now()
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            c.Inc()
            wg.Done()
        }()
    }
    wg.Wait()
    end := time.Now()
    fmt.Println(c.Load(), end.Sub(start))
}
func main() {
    c1 := CommonCounter{} // 非并发安全
    test(c1)
    c2 := MutexCounter{} // 使用互斥锁实现并发安全
    test(&c2)
    c3 := AtomicCounter{} // 并发安全且比互斥锁效率更高
    test(&c3)
}


TCP通信

TCP服务端

package main
import (
    "bufio"
    "fmt"
    "net"
)
// TCP server端
// 处理函数
func process(conn net.Conn) {
    defer conn.Close() // 处理完之后关闭连接
    for {
        reader := bufio.NewReader(conn)
        var buf [128]byte             //定义数组
        n, err := reader.Read(buf[:]) // 读取数据, 数组转成了切片
        if err != nil {
            fmt.Println("read from client failed, err:", err)
            break
        }
        recvStr := string(buf[:n])
        if recvStr == "exit" {
            break
        }
        fmt.Println("收到client端发来的数据:", recvStr) //打印接收到的数组
        conn.Write([]byte(recvStr))             // 发送数据, 把数据发回给客户端
    }
}
func main() {
    listen, err := net.Listen("tcp""127.0.0.1:20000") //启动监听, 并指定类型与端口
    if err != nil {
        fmt.Println("listen failed, err:", err)
        return
    }
    for { //一直监听
        conn, err := listen.Accept() // 建立连接。一直阻塞,直到有人连接
        if err != nil {
            fmt.Println("accept failed, err:", err)
            continue //如果连接出错, 就跳过后面的启动goroutine, 进行下一次等待连接
        }
        go process(conn) // 启动一个goroutine处理连接
    }
}


TCP客户端

package main
import (
    "bufio"
    "fmt"
    "net"
    "os"
    "strings"
)
// tcp客户端
func main() {
    conn, err := net.Dial("tcp""127.0.0.1:20000") //建立连接, Dial(拨号)
    if err != nil {
        fmt.Println("err :", err)
        return
    }
    defer conn.Close() // 关闭连接
    inputReader := bufio.NewReader(os.Stdin)
    for {
        fmt.Print("发送:")
        input, _ := inputReader.ReadString('\n')  // 读取用户输入
        inputInfo := strings.Trim(input, "\r\n")  //去掉指定字符
        if strings.ToUpper(inputInfo) == "exit" { // 如果输入exit就退出
            return
        }
        _, err = conn.Write([]byte(inputInfo)) // 发送数据
        if err != nil {
            return
        }
        //从服务器接收返回的消息
        buf := [512]byte{}
        n, err := conn.Read(buf[:])
        if err != nil {
            fmt.Println("recv failed, err:", err)
            return
        }
        fmt.Println("受到服务器回复:接收", string(buf[:n]), "成功")
    }
}

UDP通信

UDP服务端

package main
import (
    "fmt"
    "net"
)
// UDP server端
func main() {
    listen, err := net.ListenUDP("udp", &net.UDPAddr{ //启动监听, 并设置地址
        IP:   net.IPv4(0, 0, 0, 0),
        Port: 30000,
    })
    if err != nil {
        fmt.Println("listen failed, err:", err)
        return
    }
    defer listen.Close()
    for {
        var data [1024]byte
        n, addr, err := listen.ReadFromUDP(data[:]) // 接收数据
        if err != nil {
            fmt.Println("read udp failed, err:", err)
            continue
        }
        fmt.Printf("data:%v addr:%v count:%v\n", string(data[:n]), addr, n)
        _, err = listen.WriteToUDP(data[:n], addr) // 发送数据
        if err != nil {
            fmt.Println("write to udp failed, err:", err)
            continue
        }
    }
}

UDP客户端

package main
import (
    "fmt"
    "net"
)
// UDP server端
func main() {
    listen, err := net.ListenUDP("udp", &net.UDPAddr{ //启动监听, 并设置地址
        IP:   net.IPv4(0, 0, 0, 0),
        Port: 30000,
    })
    if err != nil {
        fmt.Println("listen failed, err:", err)
        return
    }
    defer listen.Close()
    for {
        var data [1024]byte
        n, addr, err := listen.ReadFromUDP(data[:]) // 接收数据
        if err != nil {
            fmt.Println("read udp failed, err:", err)
            continue
        }
        fmt.Printf("data:%v addr:%v count:%v\n", string(data[:n]), addr, n)
        _, err = listen.WriteToUDP(data[:n], addr) // 发送数据
        if err != nil {
            fmt.Println("write to udp failed, err:", err)
            continue
        }
    }
}