Go语言接着学

按照官方教程学学Go,顺带加一些自己的笔记

Posted by Haiming on February 25, 2020

学学Go,顺带记录一些自己的笔记。

基础知识

Named return values

Go 的返回值可以被命名。但是在语言之中引入了一种机制,使其如果没有命名也不会报错。

如果没有命名,那么直接就会按照函数之中引入的顺序来进行返回,比如下面这段代码:

func split(sum int) (x, y int) {
	x = sum * 4 / 9
	y = sum - x
	return
}

则其返回值就是按照顺序进行返回,此处为x,y 这一个顺序进行返回。

但是,在大型项目之中不建议这种 “naked” return 的方法,因为很难维护。

Go 之中的基本类型

一般而言,和 Java 等等区别不大,只是其将 string 算做了基本类型,并且将有符号的 int 和 无符号的 uint 之中的位数标的比较清楚。

bool

string

int  int8  int16  int32  int64
uint uint8 uint16 uint32 uint64 uintptr

byte // alias for uint8

rune // alias for int32
     // represents a Unicode code point

float32 float64

complex64 complex128

其中有一个类型 uintptr:

// uintptr is an integer type that is large enough to hold the bit pattern of
// any pointer.
type uintptr uintptr

官方建议,在系统之中,除非真的需要对位数进行要求,都应该直接调用比如int 或者是 uint这种不指定位数的值。

Go 之中的变量声明部分

原因是我写了这样的一段代码:

const (
	Big = 1 << 100
)

func main() {
	fmt.Println(Big)
	fmt.Println(reflect.TypeOf(Big))
}

如果我不在main里面加入这两句话,那么就不会报错。如果加入了,会直接报错Overflow。虽然说在64位系统之中int最大也只是64位,肯定会溢出,但是不理解为什么我不加入这两几句就不会报错。

问了同事之后发现,虽然说如果声明的变量在 Go 之中没有使用,那么 Go 就会报错,但是在 const() 之中声明的部分没有使用是不会报错的。在Go 之中还有另一个机制,如果在代码部分没有使用到某个变量,那么其就不会被编译。两个机制一起导致了这样的结果。

Stacking defers

如果有几个defer,会按照先入后出的栈顺序输出。实际上defer的操作会被压入一个栈,然后在代码块之中的其他程序执行完毕后再从栈之中得到值。注意其“执行完毕”是在函数层面,也就是当前函数的其他代码都执行完毕才会开始出栈操作。比如下面:

package main

import "fmt"

func main() {
	fmt.Println("counting")

	for i := 0; i < 10; i++ {
		defer fmt.Println(i)
	}

	fmt.Println("done")
}

实际上的输出是:

counting
done
9
8
7
6
5
4
3
2
1
0

Pointers

Go 之中有指针存在。下面代码:

package main

func main() {
	a4 := 5
	j := &a4
	println(j)
	println(*j)
}

输出为:

0xc000040740
5

但是和C不同,Go不支持指针的算数运算。

Pointer和参数传参问题

在Go之中,参数传的是值,那么在其的参数位置,和 java 不同,直接会将对象的值传入。如果想要实现修改对象的值的函数,需要将对象的指针传入函数。

Pointers to structs

在面对结构体的指针的时候,本来我们需要使用(*p).X 才能取到结构体的X的值,但是为了避免这种笨重的写法,Go将其简化成直接使用指针也可,比如p.X

Slices are like references to arrays

Slice并不是新建一个 string, 而是在原来的 string 上面按照片段来取。 如果修改了某个 string 的 slice, 会直接将原来的值也一起修改掉。比如:

package main

import "fmt"

func main() {
	names := [4]string{
		"John",
		"Paul",
		"George",
		"Ringo",
	}
	fmt.Println(names)

	a := names[0:2]
	b := names[1:3]
	fmt.Println(a, b)

	b[0] = "XXX"
	fmt.Println(a, b)
	fmt.Println(names)
}

得到的结果就是:

[John Paul George Ringo]
[John Paul] [Paul George]
[John XXX] [XXX George]
[John XXX George Ringo]

Slice defaults

结合下面的结果,我觉得能够更好的理解 Slice 这个概念:

Slice 可以当成一个只是指向原数组的一个指针,其值不论怎么修改,都只是指在头部,而值的改变要主动去指定。

package main

import "fmt"

func main() {
	s := []int{2, 3, 5, 7, 11, 13}

	s = s[0:1]
	fmt.Println(s)

	s = s[:5]
	fmt.Println(s)

	s = s[1:]
	fmt.Println(s)
}

结果为:

[2]
[2 3 5 7 11]
[3 5 7 11]

这一下子给我弄懵了,在 Java 之中,每个子字符串虽然都和父字符串使用一套空间,但是彼此之间是独立的,怎么会是这种在后面的数组之中可以出现前面的数组没有出现的值呢?

但是如果将这个 slice s 当做是 string 的一个指针那么就迎刃而解了。其只是指向原字符串,并且存有开始位和结束位。那么不管这个 slice 如何修改,我都是可以在表达式之中拿到其所有元素的值。

Slice length and capacity

在 slice 之中有两个概念,一个是 length, 一个是 capacity。下面先搞一个示例代码:

package main

import "fmt"

func main() {
	s := []int{2, 3, 5, 7, 11, 13}
	printSlice(s)

	// Slice the slice to give it zero length.
	s = s[:0]
	printSlice(s)

	// Extend its length.
	s = s[:4]
	printSlice(s)

	// Drop its first two values.
	s = s[2:]
	printSlice(s)
}

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

然后再搞一发输出:

len=6 cap=6 [2 3 5 7 11 13]
len=0 cap=6 []
len=4 cap=6 [2 3 5 7]
len=2 cap=4 [5 7]

那么其区别是啥子呢?

length 属性之中显示的是 slice 之中的数组的长度,而 cap 指的是 父数组 之中,从slice 的第一个元素开始会有多少个元素。那么只要是从0开始的slice,其 cap 都会是和父数组一样,而不是从0开始的slice,其 cap 就是从slice之中第一个元素开始直到父数组的结束的长度。

Go顺手一记

  1. 在 Go 之中,&^ 起到什么作用?

    即z = x &^ y运算相当于先把y取反(针对y的每个bit:0变成1,1变成0),然后再和x进行&运算。

    下面是一段 Abs 的代码实现:

    func Abs(x float64) float64 {
    	return Float64frombits(Float64bits(x) &^ (1 << 63))
    }