Go知识记录

2019/10/09 Go

简介

目前正在学 #Go ,做下记录,温故而知新,初学coding的时候可以快速翻查用法,了解原理。

多重赋值

多重赋值时,变量的左值和右值按从左到右的顺序赋值

var a int = 100
b := 200
b,a = a,b       // 实现交换

常量

iota

golang语言的常量计数器,只能在常量的表达式中使用。

iota在const关键字出现时将被重置为0(const内部的第一行之前),const中每新增一行常量声明将使iota计数一次(iota可理解为const语句块中的行索引)。

  • iota只能在常量的表达式中使用

  • 每次 const 出现时,都会让 iota 初始化为0.【自增长】

    const a = iota 			// a=0
    const (
      b = iota          //b=0
      c                 //c=1
    )
    
  • 自定义类型,如time包

    type Weekday int
      
    const (
        Sunday Weekday = iota
        Monday
        Tuesday
        Wednesday
        Thursday
        Friday
        Saturday
    )	
    
  • 可跳过的值

    type AudioOutput int
      
    const (
        OutMute AudioOutput = iota // 0
        OutMono                       // 1
        OutStereo                     // 2
        _
        _
        OutSurround                // 5
    )
    

匿名变量

匿名变量 ‘_‘表示,使用匿名变量时,只需要在变量声明的地方使用下划线替换即可。

a,_ = Get_data()

匿名变量不占用空间,不会分配内存。匿名变量与匿名变量之间不会因为多次声明而无法使用。

数据类型

整形(int8,int16,int32,int64)、浮点型、布尔型、字符串、数组、切片、结构体、函数(go语言的一种数据类型,可对函数类型的变量进行赋值和获取)、map、通道(channel)

字符

  • unit8类型,或者叫byte类型,ASCII码的一个字符;
  • rune类型(int32),代表一个UTF-8字符。使用fmt.sprintf中的“%T”动词可以输出变量的实际类型。

数组

var variable_name [SIZE] variable_type

数组是一串固定长度的连续内存区域。

数组可以在声明时使用初始化列表进行元素设置

var team = [3]string{"hammer","soldier","mum"}

根据元素个数确定数组大小

var team = [...]string{"hammer","soldier","mum"}   // ...表示让编译器确定数组大小

遍历数组

for k,v := range team{
    fmt.Println(k,v)
}

切片

https://www.cnblogs.com/followyou/p/12349883.html

map

https://www.cnblogs.com/followyou/p/12355313.html

函数

https://www.cnblogs.com/followyou/p/12348259.html

结构体

https://www.cnblogs.com/followyou/p/12358919.html

接口(interface)

https://www.cnblogs.com/followyou/p/12601846.html

分类

Named Type

通过 type 关键字为一个已有的 type 起个别名,像这样 type NewType ExistingType, NewType 就是名字。

Named Type 有两类:

  • pre-declarered types(内置类型),比如 int, int64, float, string, bool 等;
  • 使用关键字 type 声明的类型;

pre-declarered types这些基础的数据又可以进一步构成更复杂的类型 array,struct,map,slice,channel 等。

特征:

  • 作为一个 function 的 receiver 时,它就拥有了自己的方法;unamed types 则不能;pre-declare types 不能拥有自己的方法,要想定义自己的方法必须定义别名。
  • 属性继承。Named type 不会从它的 underlying type 或 引用 type 继承 method,但是会继承 field;
type T int

func F(t T) {}

func main() {
    var q int
    F(q)
}
// 编译失败
type T []int

func F(t T) {}

func main() {
    var q []int
    F(q)
}
// 编译成功

UnNamed Type

是一个 literal type,也就是没有名字,只有 type 本身,像这样 [6]int 没有名字。

type embedding

如果一个 type T‘ 被嵌入另一个 type T 作为它的 filed,T’ 的所有 field 和 method 都可以在 T 中使用,这种方法称之为 type embedding。

underlying type

每一个类型都有自己的 Underlying type ,如果 T 是 pre-declared type 或者 type literal,它们对应的 underlying type 就是自身 T,否则 T 的 underlying type 是 T 定义时引用的类型的 underlying type。

  • 如果两个 type 都是 named type ,彼此之间不能相互赋值;
  • 如果两个 type 其中一个是 unamed type,彼此之间可以相互赋值;

总结理解:如果为一个类型起了名字,说明你想要做区分,所以两个 named types 即使 underlying name 相同也是不能相互赋值的。

参考:理解 go types

指针

go拆分两个核心概念

  • 类型指针 允许对这个指针类型的数据进行修改。传递数据使用指针,而无需拷贝数据。类型指针不能进行偏移和运算
  • 切片 由指向起始元素的原始指针、元素数量和容量组成

go的指针类型变量拥有指针的高效访问,但又不会发生指针偏移,从而避免非法修改关键性数据问题。同时,垃圾回收也比较容易对不会发生偏移的指针进行检索和回收。

切片比原始指针具备更强大的特性,更为安全。切片发生越界时,运行时会报出宕机,并打出堆栈,而原始指针只会崩溃。

var 关键字直接定义的指针变量是不能进行赋值操作的,因为它的值为 nil,也就是还没有指向的内存地址

优点:

  • 可以修改指向数据的值;
  • 在变量赋值,参数传值的时候可以节省内存。

指针地址和指针类型

ptr := &v //v的类型为T  &v取地址 &取地址操作符

//v代表被取地址的变量,被取地址的v使用ptr变量进行接收,ptr的类型为*T,称作T的指针类型。
// * 代表指针

变量、指针和地址关系:每个变量都拥有地址,指针的值就是地址

从指针获取指针指向的值

& 获取这个变量的指针

  • 指针取值

&取出地址,*根据地址取出地址指向的值(解引用)

变量、指针地址、指针变量、取地址、取值关系:

  • 对变量进行取地址(&)操作,可以获得这个变量的指针变量
  • 指针变量的值是指针地址
  • 对指针变量进行取值(*)操作,可以获得指针变量指向的原变量的值

*操作符根本意义就是操作指针指向的变量。当操作在右值时,就是取指向变量的值;当操作在左值时,就是将值设置给指向的变量

指针接受者

  • 如果接收者类型是 map、slice、channel 这类引用类型,不使用指针;
  • 如果需要修改接收者,那么需要使用指针;
  • 如果接收者是比较大的类型,可以考虑使用指针,因为内存拷贝廉价,所以效率高。

类型转换

T(表达式)

T代表要转换的类型。、

var a int32 = 1047483647
b := int16(a)

堆栈知识

  • 栈区(stack) –存储参数值、局部变量,维护函数调用关系等。
  • 堆区(heap) –动态内存区域,随时申请和释放,程序自身要对内存泄漏负责
  • 全局区(静态区) –存储全局和静态变量
  • 字面量区 –常量字符串存储区
  • 程序代码区 –存储二进制代码
int a=o;    //全局变量
char *p1;   //全局未初始化区
main() {
    static int b=0;   //全局初始化区
    int c;  //栈
    char d[] = "abc:;  //栈
    char *p2;          //栈
    char *p3 = "hello";   //hello在常量区,p3在栈上
    p1 = (char*)malloc(10);
    p2 = (char*)malloc(20);     //分配得来的10和20字节的区域就在堆区
    strcpy(p1,"hello");        //hello放在常量区,编译器可能会将它与p3所指向的hello优化成一个地方
}

总体来讲,栈上的变量是局部的,随着局部空间的销毁而销毁,由系统负责。

堆上的变量可以提供全局访问,需要自行处理其声明周期。

注意

  • struct指针调用非引用类型的方法会自动解引用,如果是个nil指针会panic;
  • 不要对 map、slice、channel 这类引用类型使用指针;
  • 如果需要修改方法接收者内部的数据或者状态时,需要使用指针;
  • 如果需要修改参数的值或者内部数据时,也需要使用指针类型的参数;
  • 如果是比较大的结构体,每次参数传递或者调用方法都要内存拷贝,内存占用多,这时候可以考虑使用指针;
  • 像 int、bool 这样的小数据类型没必要使用指针;
  • 如果需要并发安全,则尽可能地不要使用指针,使用指针一定要保证并发安全;
  • 指针最好不要嵌套,也就是不要使用一个指向指针的指针,虽然 Go 语言允许这么做,但是这会使你的代码变得异常复杂。

参数传递

In a function call, the function value and arguments are evaluated in the usual order. After they are evaluated, the parameters of the call are passed by value to the function and the called function begins execution. 文档地址:https://golang.org/ref/spec#C…

官方文档明确说明:Go里边函数传参只有值传递一种方式

值传递

是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。

func main() {
    a := 10
    fmt.Printf("%#v\n", &a) // (*int)(0xc420018080)
    vFoo(a)
}

func vFoo(b int) {
    fmt.Printf("%#v\n", &b) // (*int)(0xc420018090)
}

指针传递

形参为指向实参地址的指针,当对形参的指向操作时,就相当于对实参本身进行的操作

func main() {
    a := 10
    pa := &a
    fmt.Printf("value: %#v\n", pa) // value: (*int)(0xc420080008)
    fmt.Printf("addr: %#v\n", &pa) // addr: (**int)(0xc420088018)
    pFoo(pa)
}

func pFoo(p * int) {
    fmt.Printf("value: %#v\n", p) // value: (*int)(0xc420080008)
    fmt.Printf("addr: %#v\n", &p) // addr: (**int)(0xc420088028)
}

定义了一个变量 a,并把地址保存在指针变量 pa 里边了。按照我们定的结论,Go中只有值传递,那么指针变量pa传给函数的形参p后,形参将会是它在栈上的一份拷贝,他们本身将各自拥有不同的地址,但是二者的值是一样的(都是变量a的地址)。上面的注释部分是我程序运行后的结果,pa 与 p 的地址各自互不相关,说明在参数传递中发生了值拷贝。

Go中没有引用传递

所谓引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

void rFoo(int & ref) {
    printf("%p\n", &ref);// 0x7ffee5aef768
}

int main() {
    int a = 10;
      printf("%p\n", &a);// 0x7ffee7307768
    int & b = a;
    printf("%p\n", &b);// 0x7ffee5aef768
    rFoo(b);
    return 0;
}

这里 b 是 a 的别名(引用),因此a、b必定具备相同的地址。那么按照引用传递的定义,实参 b 传给形参 ref 之后,ref 将是 b 的别名(也即a、b、ref都是同一个变量),他们将拥有相同地址。通过在 rFoo 函数中的打印信息,可以看到三者具有完全形同的地址,这是所谓的引用传递。

Go中函数调用只有值传递,但是类型引用有引用类型,他们是:slicemapchannel。来看看官方的说法:

There’s a lot of history on that topic. Early on, maps and channels were syntactically pointers and it was impossible to declare or use a non-pointer instance. Also, we struggled with how arrays should work. Eventually we decided that the strict separation of pointers and values made the language harder to use. Changing these types to act as references to the associated, shared data structures resolved these issues. This change added some regrettable complexity to the language but had a large effect on usability: Go became a more productive, comfortable language when it was introduced.

总结

  • Go 中函数传参仅有值传递一种方式;
  • slicemapchannel都是引用类型,但是跟c++的不同;
  • slice能够通过函数传参后,修改对应的数组值,是因为 slice 内部保存了引用数组的指针,并不是因为引用传递。

关键字

defer

defer用于资源的释放,会在函数返回之前进行调用

var a,b := 1,2
c := func(a,b int) (c int){
		c  = a+b
		defer func() {
			c = c*2
		}()
		return
	}
//6

流程判断

  • 条件判断 if
  • 条件循环 for
  • 健值循环 for range
  • 分支选择 switch switch 默认情况下 case 最后自带 break 语句,匹配成功后就不会执行其他 case,如果我们需要执行后面的 case,可以使用 fallthrough
    switch var1 {
      case val1:
          fallthrough
      case val2:
          ...
      default:
          ...
    }
    
  • 跳转 goto
  • 跳出循环 break 和 继续循环 continue

健值循环(for range)

可以使用for range遍历数组、切片、字符串、map、及chnnel。

通过for range遍历返回值规律:

  • 数组、切片和字符串返回索引和值;
  • map返回健和值
  • chnnel只返回通道内的值

遍历切片、数组

for key, value := range []int{1, 2, 3, 4} {
		fmt.Printf("key%d, value %d\n",key ,value)
}

遍历获得字符

	str := "hello"
	for key, value := range str {
		fmt.Printf("key%d, value 0x%x \n",key ,value)  //%x	十六进制,小写字母,每字节两个字符
	}

遍历map

	m := map[string]int{
		"a" : 1,
		"b" : 2,
	}
	for key, value := range m {
		fmt.Println(key,value)
	}

遍历channel –从通道接收数据

c := make(chan int)
go func(){
		//往通道内推送数据,然后结束并关闭通道
		c <- 1
		c <- 2
		c <- 3
		close(c)
}()

for value :=range c {
		...
}

编译

export GOPATH=/home/golang/code
go build -o main /goinstall                 # 按包编译

go build -o mian main.go lib.go             # 文件列表编译

go run

命令会编译源码,并且直接执行源码的main()函数,不会在当前目录留下可执行文件。

go run不会在运行目录下生成任何文件,可执行文件被放在临时文件中被执行,工作目录被设置为当前目录。 在go run的后部可以添加参数,这部分参数会作为代码可以接受的命令行输入提供给程序。 go run不能使用“go run + 包”的方式进行编译,如需快速编译运行包,需要使用如下步骤来代替:

  • 使用go build生成可执行文件。
  • 运行可执行文件。

go install

功能和go build类似,附加参数绝大多数都可以与go build通用。go install只是将编译的中间文件放在GOPATH的pkg目录下,以及固定地将编译结果放在GOPATH的bin目录下。

go install的编译过程有如下规律:

  • go install是建立在GOPATH上的,无法在独立的目录里使用go install。
  • GOPATH下的bin目录放置的是使用go install生成的可执行文件,可执行文件的名称来自于编译时的包名。
  • go install输出目录始终为GOPATH下的bin目录,无法使用-o附加参数进行自定义。
  • GOPATH下的pkg目录放置的是编译期间的中间文件。

go test

go test指定文件时默认执行文件内的所有测试用例。可以使用-run参数选择需要的测试用例单独执行

-v,可以让测试时显示详细的流程

-run跟随的测试用例的名称支持正则表达式,使用-run Test A$即可只执行Test A测试用例(假设TestA、TestAk、TestB、TestC会执行TestA、TestAk)。

go test -v -run testA main_test.go

make、new

分配内存

new(T)会为T类型的新值分配已置0的内存空间,并返回地址(指针)。适用值类型(数组、结构体)。

make(T,args)返回初始化之后的T类型的值,这个值是经过初始化后的T的引用。只适用slice、map、channel。

obsidian://open?vault=_posts&file=Go%2F2019-10-09-go-var-life

Search

    Table of Contents