Golang笔记

golang笔记

这篇文档零散的记载了一些知识点以及容易犯错的语法知识

结构体绑定方法

结构体绑定方法分为两种情况:指针绑定和赋值绑定,下面是两种情况的地址情况

import "fmt"
import "unsafe"

type MyStruct struct {
    id int
}

func (self MyStruct) Assignment() {
    fmt.Println(unsafe.Pointer(&self))
}

func (self MyStruct) modify_value_assignment() {
    self.id = 2
}

func (self *MyStruct) Pointer() {
    fmt.Println(unsafe.Pointer(&self))
}

func (self *MyStruct) modify_value_pointer() {
    self.id = 2
}

func main() {

    s0 := MyStruct{1}
    fmt.Println(s0)
    fmt.Println(unsafe.Pointer(&s0)) //打印地址

    s0.modify_value_assignment()
    fmt.Println(s0) // {1}
    s0.Assignment()

    s0.modify_value_pointer()
    fmt.Println(s0) // {2}
    s0.Pointer()
}

执行的输出是  
{1}  
0xc420084008  
{1}  
0xc420084038  
{2} 
0xc420092020  

由此可见

  • 指针绑定 拷贝了指向结构体的指针,二者指向同一块内存
  • 赋值绑定 拷贝了整个结构体变量(内存),新的结构体变量改变与原变量无关

slice 与 map

  • slice
    切片本身并不是动态数组或者数组指针。它内部实现的数据结构通过指针引用底层数组,设定相关属性将数据读写操作限定在指定的区域内。切片本身是一个只读对象,其工作机制类似数组指针的一种封装。
    切片(slice)是对数组一个连续片段的引用,所以切片是一个引用类型(因此更类似于 C/C++ 中的数组类型,或者 Python 中的 list 类型)。这个片段可以是整个数组,或者是由起始和终止索引标识的一些项的子集。需要注意的是,终止索引标识的项不包括在切片内。切片提供了一个与指向数组的动态窗口。
    给定项的切片索引可能比相关数组的相同元素的索引小。和数组不同的是,切片的长度可以在运行时修改,最小为 0 最大为相关数组的长度:切片是一个长度可变的数组。
    Slice 的数据结构定义如下:
type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

切片的结构体由3部分构成,Pointer 是指向一个数组的指针,len 代表当前切片的长度,cap 是当前切片的容量。cap 总是大于等于 len 的。

  • map
    hashmap

new 与 make

  • new
    是一个用来分配内存的内建函数,但是与C++不一样的是,它并不初始化内存,只是将其置零。也就是说,new(T)会为T类型的新项目,分配被置零的存储,并且返回它的地址,一个类型为*T的值。在Go的术语中,其返回一个指向新分配的类型为T的指针,这个指针指向的内容的值为zerovalue(zerovalue指的是各个类型的默认初始值),注意并不是指针为零,而是指针所指的内存.
    比如,对于bool类型,零值为false;int的零值为0;string的零值是空字符串:
b := new(bool)
fmt.Println(*b)
i := new(int)
fmt.Println(*i)
输出:
false
0
  • make
    内建函数make(T,len,cap)与new(T)的用途不一样。它只用来创建slice,map和channel,并且返回一个初始化的(而不是置零)类型为T的值(而不是*T)。
    之所以有所不同,是因为这三个类型的背后引用了使用前必须初始化的数据结构。例如,slice是一个三元描述符,包含一个指向数据(在数组中)的指针,长度,以及容量,在这些项被初始化之前,slice都是nil的。对于slice,map和channel,make初始化这些内部数据结构,并准备好可用的值。

goroutine 与 channel

  • goroutine
    类似我们熟知的线程,但是更轻。
    以下的程序,我们串行地去执行两次loop函数:
func loop() {
    for i := 0; i < 10; i++ {
        fmt.Printf("%d ", i)
    }
}
func main() {
    loop()
    loop()
}

毫无疑问,输出会是这样的:
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
下面我们把一个loop放在一个goroutine里跑,我们可以使用关键字go来定义并启动一个goroutine:

func main() {
    go loop() // 启动一个goroutine
    loop()
}

这次的输出变成了:
0 1 2 3 4 5 6 7 8 9
可是为什么只输出了一趟呢?明明我们主线跑了一趟,也开了一个goroutine来跑一趟啊。
原来,在goroutine还没来得及跑loop的时候,主函数已经退出了。
main函数退出地太快了,我们要想办法阻止它过早地退出,一个办法是让main等待一下:

func main() {
    go loop()
    loop()
    time.Sleep(time.Second) // 停顿一秒
}

这次确实输出了两趟,目的达到了。
可是采用等待的办法并不好,如果goroutine在结束的时候,告诉下主线说“Hey, 我要跑完了!”就好了, 即所谓阻塞主线的办法,回忆下我们Python里面等待所有线程执行完毕的写法:

for thread in threads:
    thread.join()

是的,我们也需要一个类似join的东西来阻塞住主线。那就是信道,将在下面的内容介绍。

  • channel
    channel是goroutine之间互相通讯的东西。类似我们Unix上的管道,用来goroutine之间发消息和接收消息。其实,就是在做goroutine之间的内存共享。
    • 无缓冲信道
      使用make来建立一个信道:
    var channel chan int = make(chan int)   
        // 或
        channel := make(chan int)
        ```
        那如何向信道存消息和取消息呢? 一个例子:
        ```
    func main() {
        var messages chan string = make(chan string)
    
        go func(message string) {
            messages <- message // 存消息
        }("Ping!")
    
        fmt.Println(<-messages) // 取消息
    }
    
    默认的,信道的存消息和取消息都是阻塞的,叫做无缓冲的信道,也就是说, 无缓冲的信道在取消息和存消息的时候都会挂起当前的goroutine,除非另一端已经准备好。 比如以下的main函数和foo函数:
    var ch chan int = make(chan int)
    
    func foo() {
        ch <- 0  // 向ch中加数据,如果没有其他goroutine来取走这个数据,那么挂起foo, 直到main函数把0这个数据拿走
    }
    
    func main() {
        go foo()
        <- ch // 从ch取数据,如果ch中还没放数据,那就挂起main线,直到foo函数中放数据为止
    }
    
    那既然信道可以阻塞当前的goroutine, 那么「如何让goroutine告诉主线我执行完毕了」 的问题, 使用一个信道来告诉主线即可:
    var complete chan int = make(chan int)
    
    func loop() {
        for i := 0; i < 10; i++ {
            fmt.Printf("%d ", i)
        }
        complete <- 0 // 执行完毕了,发个消息
    }
    
    func main() {
        go loop()
        <- complete // 直到线程跑完, 取到消息. main在此阻塞住
    }
    
    如果不用信道来阻塞主线的话,主线就会过早跑完,loop线都没有机会执行

    无缓冲信道从不存储数据,流入的数据必须要流出才可以。
    观察以下的程序:
    var ch chan int = make(chan int)
    
    func foo(id int) { //id: 这个routine的标号
        ch <- id
    }
    
    func main() {
        // 开启5个routine
        for i := 0; i < 5; i++ {
            go foo(i)
        }
        // 取出信道中的数据
        for i := 0; i < 5; i++ {
            fmt.Print(<- ch)
        }
    }
    
    我们开了5个goroutine,然后又依次取数据。其实整个的执行过程细分的话,5个线的数据 依次流过信道ch, main打印之, 而宏观上我们看到的即 无缓冲信道的数据是先到先出,但是 无缓冲信道并不存储数据,只负责数据的流通.

    • 缓冲信道
      缓冲这个词意思是,缓冲信道不仅可以流通数据,还可以缓存数据。它是有容量的,存入一个数据的话 , 可以先放在信道里,不必阻塞当前线而等待该数据取走。
      当缓冲信道达到满的状态的时候,就会表现出阻塞了,因为这时再也不能承载更多的数据了,「你们必须把 数据拿走,才可以流入数据」。
      在声明一个信道的时候,我们给make以第二个参数来指明它的容量(默认为0,即无缓冲):
      var ch chan int = make(chan int, 2) // 写入2个元素都不会阻塞当前goroutine
      存储个数达到2的时候会阻塞
      如下的例子,缓冲信道ch可以无缓冲的流入3个元素:
    func main() {
        ch := make(chan int, 3)
        ch <- 1
        ch <- 2
        ch <- 3
    }
    
    如果你再试图流入一个数据的话,信道ch会阻塞main线, 报死锁。也就是说,缓冲信道会在满容量的时候加锁。
    其实,缓冲信道是先进先出的,我们可以把缓冲信道看作为一个线程安全的队列:
    func main() {
        ch := make(chan int, 3)
        ch <- 1
        ch <- 2
        ch <- 3
    
        fmt.Println(<-ch) // 1
        fmt.Println(<-ch) // 2
        fmt.Println(<-ch) // 3
    }
    
    • 信道数据读取和信道关闭
      你也许发现,上面的代码一个一个地去读取信道简直太费事了,Go语言允许我们使用range来读取信道:
    func main() {
        ch := make(chan int, 3)
        ch <- 1
        ch <- 2
        ch <- 3
    
        for v := range ch {
            fmt.Println(v)
        }
    }
    

Interface

  • 用法一:
    假设某公司有两个员工,一个普通员工和一个高级员工,但是基本薪资是相同的,高级员工多拿奖金。计算公司为员工的总开支。
// 薪资计算器接口
type SalaryCalculator interface {
    CalculateSalary() int
}
// 普通员工
type Contract struct {
    empId  int
    basicpay int
}
// 有技术证的员工
type Permanent struct {
    empId  int
    basicpay int
    jj int // 奖金
}

func (p Permanent) CalculateSalary() int {
    return p.basicpay + p.jj
}

func (c Contract) CalculateSalary() int {
    return c.basicpay
}
// 总开支
func totalExpense(s []SalaryCalculator) {
    expense := 0
    for _, v := range s {
        expense = expense + v.CalculateSalary()
    }
    fmt.Printf("总开支 $%d", expense)
}

func main() {
    pemp1 := Permanent{1,3000,10000}
    pemp2 := Permanent{2, 3000, 20000}
    cemp1 := Contract{3, 3000}
    employees := []SalaryCalculator{pemp1, pemp2, cemp1}
    totalExpense(employees)
}
  • 用法二:
type Test interface {
    Tester()
}

type MyFloat float64

func (m MyFloat) Tester() {
    fmt.Println(m)
}

func describe(t Test) {
    fmt.Printf("Interface 类型 %T ,  值: %v\n", t, t)
}

func main() {
    var t Test
    f := MyFloat(89.7)
    t = f
    describe(t)
    t.Tester()
}
  • 空接口
    具有0个方法的接口称为空接口。它表示为interface{}。由于空接口有0个方法,所有类型都实现了空接口。
func describe(i interface{}) {
    fmt.Printf("Type = %T, value = %v\n", i, i)
}

func main() {
    // 任何类型的变量传入都可以
    s := "Hello World"
    i := 55
    strt := struct {
        name string
    }{
        name: "Naveen R",
    }
    describe(s)
    describe(i)
    describe(strt)
}

### defer,panic和recover - defer 在函数return之后,返回调用它的函数之前运行 - panic 当golang中遇到panic时,如果不进行recover,便会导致整个程序挂掉 - recover golang 的错误处理流程:当一个函数在执行过程中出现了异常或遇到panic(),正常语句就会立即终止,然后执行 defer语句,再报告异常信息,最后退出goroutine。如果在defer中使用了recover()函数,则会捕获错误信息,使该错误信息终止报告。 ``` func main() { i := 10000 for j := 0; j < 3; j++ { // 使用多协程处理,其中可以预见的是除数为0会抛出异常 divide(i, j) } }

func divide(i, j int) {
defer func() {
if r := recover(); r != nil {
// 这里可以对异常进行一些处理和捕获
fmt.Println(“Recovered:”, r)
}
}()
fmt.Println(i / j) // 第一次触发除0异常,panic
}

运行结果:
10000
5000

<br>
当有多个defer的时候,出现故障,此时跳转到包含recover()的defer函数执行

func main() {
defer func() {
fmt.Println(“1”)
}()
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
panic(“fault”)
fmt.Println(“2”)
}

运行结果:
fault
1


### 类型断言 与 类型判断
- 类型断言  
类型断言用于提取接口的基础值,语法:i.(T)

func assert(i interface{}){
s:= i.(int)
fmt.Println(s)
}

func main(){
var s interface{} = 55
assert(s)
}

程序打印的是int值, 但是如果我们给s 变量赋值的是string类型,程序就会panic。  
当程序改为:

func assert(i interface{}) {
v, ok := i.(int)
fmt.Println(v, ok)
}
func main() {
var s interface{} = 56
assert(s)
var i interface{} = “Steven Paul”
assert(i)
}

如果 i 的值是int类型, 那么v就是i 对应的值,ok就是true。否则ok为false,程序并不会panic。  

- 类型判断  
类型判断的语法类似于类型断言。在类型断言的语法i.(type)中,类型type应该由类型转换的关键字type替换。

func findType(i interface{}) {
switch i.(type) {
case string:
fmt.Printf(“String: %s\n”, i.(string))
case int:
fmt.Printf(“Int: %d\n”, i.(int))
default:
fmt.Printf(“Unknown type\n”)
}
}
func main() {
findType(“Naveen”)
findType(77)
findType(89.98)
}

<br>
还可以将类型与接口进行比较。如果我们有一个类型并且该类型实现了一个接口,那么可以将它与它实现的接口进行比较。

type Describer interface {
Describe()
}
type Person struct {
name string
age int
}

func (p Person) Describe() {
fmt.Printf("%s is %d years old", p.name, p.age)
}

func findType(i interface{}) {
switch v := i.(type) {
case Describer:
v.Describe()
default:
fmt.Printf(“unknown type\n”)
}
}

func main() {
findType(“Naveen”)
p := Person{
name: “Naveen R”,
age: 25,
}
findType§
}
输出结果:
unknown type
Naveen R is 25 years old

<br>
当一个类型实现了多个接口,switch case按照编写的顺序优先执行,如以下代码:

type Describer interface {
Describe()
}

type Describer2 interface {
Describe2()
}

type Person struct {
name string
age int
}

func (p Person) Describe() {
fmt.Println(“describe”)
}

func (p Person) Describe2() {
fmt.Println(“describe2”)
}

func findType(i interface{}) {
switch v := i.(type) {
case Describer2:
v.Describe2()
case Describer:
v.Describe()
default:
fmt.Printf(“unknown type\n”)
}
}

func main() {
p := Person{
name: “Naveen R”,
age: 25,
}
findType§
}

输出结果:
describe2







  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值