学习笔记

1、Go Module:

go mod init:为当前 Go 项目创建一个新的 module
go mod tidy:自动分析 Go 源码的依赖变更
go mod vendor:可以支持 vendor 机制

2、Go 包的初始化次序:
image.png
  • 依赖包按“深度优先”的次序进行初始化;
  • 每个包内按以“常量 -> 变量 -> init() -> main()”的顺序进行初始化;
  • 包内的多个 init 函数按出现次序进行自动调用(顺序执行)。
3、init 函数的用途:
  • 重置包级变量值;
  • 实现对包级变量的复杂初始化;
  • 在 init 函数中实现“注册模式”。
4、变量声明规则:

包级变量:

  • 只能使用带有 var 关键字的变量声明形式,不能使用短变量声明形式,但在形式细节上可以有一定灵活度。
var (
  a = 13
  b = int32(17)
  f = float32(3.14)
)
  • 可以将延迟初始化的变量声明放在一个 var 声明块 (比如上面的第一个 var 声明块),然后将声明且显式初始化的变量放在另一个 var 块中(比如上面的第二个 var 声明块)
var (
    netGo  bool 
    netCgo bool 
)

var (
    aLongTimeAgo = time.Unix(1, 0)
    noDeadline = time.Time{}
    noCancel   = (chan struct{})(nil)
)
  • iota常量计数器
    1、iota在const关键字出现时将被重置为0。
    2、const中每新增一行常量声明将使iota计数一次(iota可理解为const语句块中的行索引)。
const (
        n1 = iota //0
        n2        //1
        n3        //2
        n4        //3
    )
const (
        n1 = iota //0
        n2        //1
        _         //丢弃该值,常用在错误处理中
        n4        //3
    )
const (
        n1 = iota //0
        n2 = 100  //100
        n3 = iota //2
        n4        //3
    )

局部变量:

  • 延迟初始化的局部变量声明采用通用的变量声明形式
var err error
  • 声明且显式初始化的局部变量使用短变量声明形式
a := 17
f := 3.14
s := "hello, gopher!"
5、条件判断语句的特殊写法
  • 正常写法:
a := 10
if a == 1 {
    fmt.Println("a == 1")
} else if a == 2 {
    fmt.Println("a == 2")
} else if a == 3 {
    fmt.Println("a == 3")
} else {
    fmt.Println(a)
}
  • 简略写法:
if a := 10; a == 1 {
    fmt.Println("a == 1")
} else if a == 2 {
    fmt.Println("a == 2")
} else if a == 3 {
    fmt.Println("a == 3")
} else {
    fmt.Println(a)
}
6、for循环语句的特殊写法
  • 正常写法:
for i := 0; i < 10; i++ {
    fmt.Println(i)
}
  • 简略写法:
//省略初始语句和结束语句
var i = 10
for i > 0 {
    fmt.Println(i)
    i--
}
  • 死循环:
for {
    fmt.Println("wuxian")
}
  • for range循环:
for index, value := range list {
    fmt.Println("index:", index, ",", "value:", value)
}
7、数组、切片、map
  • 数组的拷贝是深拷贝,不会影响原数组:
list1 := [3]int{1, 2, 3}
list2 := list1
list2[0] = 100
fmt.Println(list1) //[1 2 3]
  • 切片的拷贝是浅拷贝,会影响原切片:
list1 := []int{1, 2, 3}
list2 := list1
list2[0] = 100
fmt.Println(list1) //[100 2 3]
  • 深拷贝切片:
a := []int{1, 2, 3, 4}
b := make([]int, 5)
copy(b, a)
b[0] = 100
fmt.Println(a) //[1 2 3 4]
  • 切片删除索引index元素:append(a[:index], a[index+1:]...)
// 切片删除下标为2的元素
a := []int{1, 2, 3, 4}
a = append(a[0:2], a[3:]...)
fmt.Println(a)
  • 切片删除多个元素
    a = append(a[:i], a[i+1:]...) // 删除中间1个元素
    a = append(a[:i], a[i+N:]...) // 删除中间N个元素

  • map基本使用
    直接在声明的时候填充元素:

userInfo := map[string]string{
    "username": "沙河小王子",
    "password": "123456",
}
  • map的遍历
func main() {
    scoreMap := make(map[string]int)
    scoreMap["张三"] = 90
    scoreMap["小明"] = 100
    scoreMap["娜扎"] = 60
    for k, v := range scoreMap {
        fmt.Println(k, v)
    }
}
8、结构体struct
  • 1、定义
type person struct {
    name, city string
    gender     string
    age        int8
}

var p person
p.name = "panda"
p.age = 21
p.gender = "女"
p.city = "BeiJing"

// p2 为结构体指针
var p2 = new(person)
fmt.Println(p2)

// 取结构体的地址进行实例化
p3 := &person{}
fmt.Println(p3)

// 结构体初始化
p4 := person{ //键值对初始化
    name:   "Panda",
    city:   "BeiJing",
    gender: "女",
    age:    21,
}
fmt.Println(p4)

p5 := person{ //列表初始化(按定义时的顺序)
    "Panda", "BeiJing", "女", 21,
}
fmt.Printf("%#v\n", p5)
  • 2、构造函数
//定义
func newPerson(name, city string, age int8) *person {
    return &person{
        name: name,
        city: city,
        age:  age,
    }
}

//调用构造函数
p9 := newPerson("张三", "沙河", 90)
fmt.Printf("%#v\n", p9) //&main.person{name:"张三", city:"沙河", age:90}
  • 3、方法和接收者(相当于结构体的成员函数)
    Go语言中的方法(Method)是一种作用于特定类型变量的函数。
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("小王子", 25)
    p1.Dream()
}
  • 4、结构体-JSON 序列化、反序列化
type student struct {
    ID     int
    Gender string
    Name   string
}

type class struct {
    Title    string
    Students []student
}

c := &class{
    Title:    "101",
    Students: make([]student, 0, 20),
}
for i := 0; i < 10; i++ {
    stu := student{
        Name:   "stu" + string(i+48),
        Gender: "男",
        ID:     i,
    }
    c.Students = append(c.Students, stu)
}
data, err := json.Marshal(c) // 序列化
if err != nil {
    fmt.Println("json marshal failed!")
    return
}
fmt.Printf("json:%s\n", data)

str := `{"Title":"101","Students":[{"ID":0,"Gender":"男","Name":"stu0"},{"ID":1,"Gender":"男","Name":"stu1"},{"ID":2,"Gender":"男","Name":"stu2"},{"ID":3,"Gender":"男","Name":"stu3"},{"ID":4,"Gender":"男","Name":"stu4"},{"ID":5,"Gender":"男","Name":"stu5"},{"ID":6,"Gender":"男","Name":"stu6"},{"ID":7,"Gender":"男","Name":"stu7"},{"ID":8,"Gender":"男","Name":"stu8"},{"ID":9,"Gender":"男","Name":"stu9"}]}`
c1 := &class{}
err = json.Unmarshal([]byte(str), c1) // 反序列化
if err != nil {
    fmt.Println("json unmarshal failed!")
    return
}
fmt.Printf("%#v\n", c1)
9、接口interface
  • 1、语法
    接口的出现是为了支持多态,例如一个函数可能会接收多种结构体作为参数,如果此时参数类型还是传统的结构体,那么就只能支持传入一种结构体类型,这时可以使用接口作为参数类型。
    只要一个类型实现了接口中规定的所有方法,那么它就实现了这个接口。
package main

import "fmt"

type dog struct {
    name string
}

func (d dog) say() {
    fmt.Println(d.name + ":汪汪汪~")
}

type cat struct {
    name string
}

func (c cat) say() {
    fmt.Println(c.name + ":喵喵喵~")
}

// 接口为了实现多态,只要实现了say方法的结构体都可以被视为sayer类型
type sayer interface {
    say()
}

// 这里传入的参数因为既有狗也有猫,所以必须使用接口类型进行多态传入
func hit(someone sayer) {
    someone.say()
}

func main() {
    dog1 := dog{
        "旺财",
    }
    cat1 := cat{
        "加菲",
    }
    hit(dog1)
    hit(cat1)
}
  • 2、接口组合
    接口与接口之间可以通过互相嵌套形成新的接口类型
// src/io/io.go

type Reader interface {
    Read(p []byte) (n int, err error)
}

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

type Closer interface {
    Close() error
}

// ReadWriter 是组合Reader接口和Writer接口形成的新接口类型
type ReadWriter interface {
    Reader
    Writer
}

// ReadCloser 是组合Reader接口和Closer接口形成的新接口类型
type ReadCloser interface {
    Reader
    Closer
}

// WriteCloser 是组合Writer接口和Closer接口形成的新接口类型
type WriteCloser interface {
    Writer
    Closer
}

只需要实现新接口类型中规定的所有方法就算实现了该接口类型

  • 3、空接口
    空接口是指没有定义任何方法的接口类型。因此任何类型都可以视为实现了空接口。也正是因为空接口类型的这个特性,空接口类型的变量可以存储任意类型的值。
    使用空接口实现可以接收任意类型的函数参数:
// 空接口作为函数参数
func show(a interface{}) {
    fmt.Printf("type:%T value:%v\n", a, a)
}

使用空接口实现可以保存任意值的字典:

// 空接口作为map值
var studentInfo = make(map[string]interface{})
studentInfo["name"] = "沙河娜扎"
studentInfo["age"] = 18
studentInfo["married"] = false
fmt.Println(studentInfo)
10、reflect 反射

reflect包提供了reflect.TypeOf和reflect.ValueOf两个函数来获取任意对象的Value和Type。

11、并发 goroutine

区别于操作系统线程由系统内核进行调度, goroutine 是由Go运行时(runtime)负责调度。例如Go运行时会智能地将 m个goroutine 合理地分配给n个操作系统线程,实现类似m:n的调度机制,不再需要Go开发者自行在代码层面维护一个线程池。

Goroutine 是 Go 程序中最基本的并发执行单元。每一个 Go 程序都至少包含一个 goroutine——main goroutine,当 Go 程序启动时它会自动创建。

启动 goroutine 的方式非常简单,只需要在调用函数(普通函数和匿名函数)前加上一个go关键字:

go func(){
  // ...
}()

为了防止main函数主线程结束时创建的goroutine 还没有运行完,就需要使用sync.WaitGroup全局等待组变量,让main函数等待其他线程运行结束

package main

import (
    "fmt"
    "sync"
)

// 声明全局等待组变量
var wg sync.WaitGroup

func hello() {
    fmt.Println("hello")
    wg.Done() // 告知当前goroutine完成
}

func main() {
    wg.Add(1) // 登记1个goroutine
    go hello()
    fmt.Println("你好")
    wg.Wait() // 阻塞等待登记的goroutine完成
}

启动多个goroutine:

package main

import (
    "fmt"
    "sync"
)

var wg sync.WaitGroup

func hello(i int) {
    defer wg.Done() // goroutine结束就登记-1
    fmt.Println("hello", i)
}
func main() {
    for i := 0; i < 10; i++ {
        wg.Add(1) // 启动一个goroutine就登记+1
        go hello(i)
    }
    wg.Wait() // 等待所有登记的goroutine都结束
}

匿名函数版本:

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1) // 启动一个goroutine就登记+1
        go func(i int) {
            fmt.Println("hello", i)
            wg.Done() // goroutine结束就登记-1
        }(i) //必须要显式的传入i,不然闭包访问到的 i 可能跟循环次数不对应
    }
    wg.Wait() // 等待所有登记的goroutine都结束
}
12、channel通信
  • 1、初始化
    通道是一种消息队列,总是遵循先进先出的原则;
    channel是一种类型,一种引用类型(空值为nil),声明格式如下:
var x1 chan int //声明一个传递整型的通道
var x2 chan bool //声明一个传递布尔型的通道
var x3 chan []int //声明一个传递int切片的通道

声明的通道需要使用make函数初始化之后才能使用,格式如下:

make(chan 元素类型, [缓冲大小])
x1 := make(chan int)
x2 := make(chan bool)
x3 := make(chan []int)
ch5 := make(chan bool, 1)  // 声明一个缓冲区大小为1的通道
  • 2、channel操作
    通道共有发送(send)、接收(receive)和关闭(close)三种操作,发送和接收操作都使用<-符号。

先使用以下语句定义一个通道:

ch := make(chan int)

①发送
将一个值发送到通道中。

ch <- 10 // 把10发送到ch中

②接收
从一个通道中接收值。

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

③关闭
我们通过调用内置的close函数来关闭通道。

close(ch)
  • 3、无缓冲的通道
    无缓冲的通道又称为阻塞的通道。
    ch := make(chan int)没有分配缓冲区大小,信息必须要手把手的同步传输
  • 4、有缓冲的通道
    ch := make(chan int, 1)
  • 5、对通道的操作
len(ch1) //取通道中的元素数量
cap(ch1) //获取通道的容量
  • 6、从通道中取值的两种方式
for res := range ch2 {
    fmt.Println(res)
}
for {
    val, ok := <-ch1
    if !ok {
        break
    }
    ch2 <- val * val
}
  • 7、单向通道
    一般用于函数传参时规定通道的方向
    ch1 chan<- int 只能接收值
    ch2 <-chan int 只能发送值
    image.png

    例如:
package main

import (
    "fmt"
)

func f1(ch1 chan<- int) {
    for i := 0; i < 100; i++ {
        ch1 <- i
    }
    close(ch1)
}

func f2(ch1 <-chan int, ch2 chan<- int) {
    for {
        val, ok := <-ch1
        if !ok {
            break
        }
        ch2 <- val * val
    }
    close(ch2)
}

func main() {
    ch1 := make(chan int, 100)
    ch2 := make(chan int, 100)

    go f1(ch1)
    go f2(ch1, ch2)

    for ret := range ch2 {
        fmt.Println(ret)
    }
}

  • 8、select多路复用:同时从多个通道接收数据
    类似于之前学到的 switch 语句,但是如果有多个case符合条件则会随机选取其中一条执行而不是顺序执行
select {
case <-ch1:
    //...
case data := <-ch2:
    //...
case ch3 <- 10:
    //...
default:
    //默认操作
}

Select 语句具有以下特点。

  • 可处理一个或多个 channel 的发送/接收操作。
  • 如果多个 case 同时满足,select 会随机选择一个执行。
  • 对于没有 case 的 select 会一直阻塞,可用于阻塞 main 函数,防止退出。
13、并发安全和锁
  • 1、互斥锁
    互斥锁是完全互斥的
var lock sync.Mutex //定义一个互斥锁类型
lock.Lock() //加锁
lock.Unlock() //释放锁
  • 2、读写互斥锁:适用于读的操作数量远远大于写操作
    读取时可以不加锁,写的时候才有必要加锁
    加了读锁之后都可以读,不可以写;
    加了写锁之后其他的线程都不能读写
var rwlock sync.RWMutex
rwlock .Lock()  //加写锁
rwlock .Unlock()  //释放写锁
rwlock .RLock()  //加读锁
rwlock .RUnlock()  //释放读锁
  • 3、sync.WaitGroup
    进程同步器
var wg   sync.WaitGroup
wg.Add(x) //计数器+x
wg.Done() //计数器-1
wg.Wait() //阻塞直到计数器变为0
  • 4、sync.Once:只执行一次
    确保某些操作即使在高并发的场景下也只会被执行一次,例如只加载一次配置文件等
func test(){
 ...
}
var once sync.Once
once.Do(test)
  • 5、sync.Map:并发安全版map
    go 语言中内置的 map 不是并发安全的,当过多的线程并发访问map时会报错:
    fatal error: concurrent map writes
// 并发安全的map
var m = sync.Map{} //不需要指定key、value类型(都是空接口类型)
m.Store(key, value)  // 存储key-value
m.Load(key) // 根据key取值
image.png
  • 6、原子操作
    通常直接使用原子操作比使用锁操作效率更高,但是只针对整数数据类型(int32、uint32、int64、uint64)
    方法:
    LoadInt32、StoreInt32、AddInt32、SwapInt32、CompareAndSwapInt32
import  "sync/atomic"
type AtomicCounter struct {
    counter int64
}

func (a *AtomicCounter) Inc() {
    atomic.AddInt64(&a.counter, 1)
}

func (a *AtomicCounter) Load() int64 {
    return atomic.LoadInt64(&a.counter)
}
14、网络编程
  • 1、TCP:
    服务端:
package main

import (
    "bufio"
    "fmt"
    "net"
)

func process1(conn net.Conn) {
    defer conn.Close() //处理完毕之后要关闭该连接
    // 针对当前连接做数据发送和接收操作
    for {
        reader := bufio.NewReader(conn)
        var buf [128]byte
        n, err := reader.Read(buf[:])
        if err != nil {
            fmt.Printf("read from conn failed, err:%v\n", err)
            break
        }
        recv := string(buf[:n])
        fmt.Println("接收到的数据:", recv)
        conn.Write([]byte("ok")) //把收到的数据返回到客户端
    }
}

func main() {
    // 1、开启服务
    listen, err := net.Listen("tcp", "127.0.0.1:20000")
    if err != nil {
        fmt.Printf("Listen failed, err:%v\n", err)
        return
    }
    for {
        // 2、等待客户端建立连接
        conn, err := listen.Accept()
        if err != nil {
            fmt.Printf("Accept failed, err:%v\n", err)
            continue
        }
        // 3、启动一个单独的goroutine去处理连接
        go process1(conn)
    }
}

客户端:

package main

import (
    "bufio"
    "fmt"
    "net"
    "os"
    "strings"
)

func main() {
    // 1、与服务端建立连接
    conn, err := net.Dial("tcp", "127.0.0.1:20000")
    if err != nil {
        fmt.Printf("Dial failed, err:%v\n", err)
        return
    }

    // 2、利用该连接进行数据发送和接收
    input := bufio.NewReader(os.Stdin)
    for {
        s, _ := input.ReadString('\n')
        s = strings.TrimSpace(s)
        if strings.ToUpper(s) == "Q" {
            return
        }
        // 给服务端发消息
        _, err := conn.Write([]byte(s))
        if err != nil {
            fmt.Printf("Send failed, err:%v\n", err)
            return
        }
        // 从服务端接收回复的消息
        var buf [1024]byte
        n, err := conn.Read(buf[:])
        if err != nil {
            fmt.Printf("Read failed, err:%v\n", err)
            return
        }
        fmt.Println("收到服务端回复:", string(buf[:n]))
    }
}
  • 2、TCP黏包:
    tcp流模式传递数据,当数据量较多时,多条数据混在一起接收时不知道是哪一条

①由Nagle算法造成的发送端的粘包:Nagle算法是一种改善网络传输效率的算法。简单来说就是当我们提交一段数据给TCP发送时,TCP并不立刻发送此段数据,而是等待一小段时间看看在等待期间是否还有要发送的数据,若有则会一次把这两段数据发送出去。
②接收端接收不及时造成的接收端粘包:TCP会把接收到的数据存在自己的缓冲区中,然后通知应用层取数据。当应用层由于某些原因不能及时的把TCP的数据取出来,就会造成TCP缓冲区中存放了几段数据。

解决办法:对数据包进行封包、拆包操作

// socket_stick/proto/proto.go
package proto

import (
    "bufio"
    "bytes"
    "encoding/binary"
)

// Encode 将消息编码
func Encode(message string) ([]byte, error) {
    // 读取消息的长度,转换成int32类型(占4个字节)
    var length = int32(len(message))
    var pkg = new(bytes.Buffer)
    // 写入消息头
    err := binary.Write(pkg, binary.LittleEndian, length)
    if err != nil {
        return nil, err
    }
    // 写入消息实体
    err = binary.Write(pkg, binary.LittleEndian, []byte(message))
    if err != nil {
        return nil, err
    }
    return pkg.Bytes(), nil
}

// Decode 解码消息
func Decode(reader *bufio.Reader) (string, error) {
    // 读取消息的长度
    lengthByte, _ := reader.Peek(4) // 读取前4个字节的数据
    lengthBuff := bytes.NewBuffer(lengthByte)
    var length int32
    err := binary.Read(lengthBuff, binary.LittleEndian, &length)
    if err != nil {
        return "", err
    }
    // Buffered返回缓冲中现有的可读取的字节数。
    if int32(reader.Buffered()) < length+4 {
        return "", err
    }

    // 读取真正的消息数据
    pack := make([]byte, int(4+length))
    _, err = reader.Read(pack)
    if err != nil {
        return "", err
    }
    return string(pack[4:]), nil
}
  • 3、UDP:
    UDP服务端:
// UDP/server/main.go

// 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客户端

// UDP 客户端
func main() {
    socket, err := net.DialUDP("udp", nil, &net.UDPAddr{
        IP:   net.IPv4(0, 0, 0, 0),
        Port: 30000,
    })
    if err != nil {
        fmt.Println("连接服务端失败,err:", err)
        return
    }
    defer socket.Close()
    sendData := []byte("Hello server")
    _, err = socket.Write(sendData) // 发送数据
    if err != nil {
        fmt.Println("发送数据失败,err:", err)
        return
    }
    data := make([]byte, 4096)
    n, remoteAddr, err := socket.ReadFromUDP(data) // 接收数据
    if err != nil {
        fmt.Println("接收数据失败,err:", err)
        return
    }
    fmt.Printf("recv:%v addr:%v count:%v\n", string(data[:n]), remoteAddr, n)
}
15、单元测试

go test命令是一个按照一定约定和组织的测试代码的驱动程序。在包目录内,所有以_test.go为后缀名的源代码文件都是go test测试的一部分,不会被go build编译到最终的可执行文件中。

_test.go文件中有三种类型的函数,单元测试函数、基准测试函数和示例函数。

image.png

go test命令会遍历所有的
_test.go文件中符合上述命名规则的函数,然后生成一个临时的main包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件。

  • 1、测试函数:
    每个测试函数必须导入testing包,测试函数的基本格式(签名)如下:
func TestName(t *testing.T){
    // ...
}

其中参数t用于报告测试失败和附加的日志信息。 testing.T的拥有的方法如下:

func (c *T) Error(args ...interface{})
func (c *T) Errorf(format string, args ...interface{})
func (c *T) Fail()
func (c *T) FailNow()
func (c *T) Failed() bool
func (c *T) Fatal(args ...interface{})
func (c *T) Fatalf(format string, args ...interface{})
func (c *T) Log(args ...interface{})
func (c *T) Logf(format string, args ...interface{})
func (c *T) Name() string
func (t *T) Parallel()
func (t *T) Run(name string, f func(t *T)) bool
func (c *T) Skip(args ...interface{})
func (c *T) SkipNow()
func (c *T) Skipf(format string, args ...interface{})
func (c *T) Skipped() bool
16、泛型

如果多个函数仅仅是参数类型不同,可以考虑使用泛型
观看以下两个函数:

func reverseInt(s []int) []int {
    l := len(s)
    r := make([]int, l)

    for i, e := range s {
        r[l-i-1] = e
    }
    return r
}
func reverseFloat64(s []float64) []float64 {
    l := len(s)
    r := make([]float64, l)

    for i, e := range s {
        r[l-i-1] = e
    }
    return r
}

仅仅是处理的参数类型不同,过程完全一样,那么可以使用泛型语法:

func min[T int | float64](a, b T) T {
    if a <= b {
        return a
    }
    return b
}

这次定义的min函数就同时支持int和float64两种类型:

m1 := min[int](1, 2)  // 1
m2 := min[float64](-0.1, -0.2)  // -0.2
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 是一门编译型语言,运行效率高,开发高效,部署简单;语言层面支持并发,易于利用多核实现并发;内置runtime(作用...
    dev_winner阅读 305评论 0 3
  • Go go run xx.go 执行go文件 go build xx.go生成二进制文件 一. GO 基础 1. ...
    为什么不让我改名字阅读 282评论 0 3
  • 使用go1.10版本,在liteIde里开发。 1,变量声明后必须使用,不然编译不过(全局变量可以不用)。 2,变...
    adrian920阅读 1,033评论 1 1
  • 1.for range结合指针如下写法输出的*v都是m的最后一个valuefor k,v := range m {...
    javid阅读 618评论 0 0
  • 1.是一种静态强类型、编译型语言,语法与C相近,功能更丰富:内存安全,GC(垃圾回收),结构形态及并发计算2.go...
    hypercode阅读 313评论 0 0