(一)Go 语言基础笔记

Go语言安装

安装 Go 需要根据自己的电脑是32位,还是64位到官网下载对应的安装包。执行安装、配置环境变量,之后可以在命令行输入go version 来验证是否安装成功。

需要配置的环境变量有

  • GOROOT:Go 语言安装根目录的路径,也就是 GO 语言的安装路径。
  • GOPATH:若干工作区目录的路径。是我们自己定义的工作空间。可以是多个目录路径,每个目录都代表 Go 语言的一个工作区(workspace)。
  • GOBIN:GO 程序生成的可执行文件(executable file)的路径。

Go 语言源码的组织方式

Go 源码以代码包位基本组织单位,代码包与目录一一对应,代码包里可以有子包,类似目录的子目录。包里的 go 文件需要声明为同一个代码包。通过 import 来导入代码包,导入路径从 src 子目录,到该包的实际存储位置的相对路径。

Go 语言的源码文件存放在环境变量 GOPATH 包含的某个工作区(目录)中的 src 目录下的某个代码包(目录)中。

Go源码安装后,产生的归档文件(以 .a为扩展名的文件)会放入工作区的pkg目录。可执行文件会放入工作区的 bin 子目录。 go 代码包安装命令

go install github.com/labstack/echo

归档文件的相对目录与 pkg 目录之间还有一级目录,叫做平台相关目录。平台相关目录的名称是由 build(也称“构建”)的目标操作系统、下划线和目标计算架构的代号组成的,如 linux_amd64 对应 Linux 操作系统64位。

Go指令

  • 构建使用命令go build
    • 作用:在当前目录编译生成一个可执行的二进制文件(依赖包生成的静态库文件放在$GOPATH/pkg),必须有main包才可以。
    • 加入标记-x,查看go build命令具体都执行操作。
    • 加入标记-n,只查看具体操作而不执行它们。
  • 加入标记-v,这样可以看到go build命令编译的代码包的名称。它在与-a标记搭配使用时很有用。
    • 加入标记-i,添加缓存效果。
  • 安装使用命令go install
    • 作用:主要用来生成库和工具,(如果有main包)编译后生成的可执行工具文件放到 bin 目录、$GOPATH/bin,编译后的库文件放到 pkg 目录下($GOPATH/pkg)。并进行缓存,如果包未做更改,下次编译则直接使用缓存。常用于安装依赖。
  • 获取包 go get
    • -u:下载并安装代码包,不论工作区中是否已存在它们。
    • -d:只下载代码包,不安装代码包。
    • -fix:在下载代码包后先运行一个用于根据当前 Go 语言版本修正代码的工具,然后再安装代码包。
    • -t:同时下载测试所需的代码包。
    • -insecure:允许通过非安全的网络协议下载和安装代码包。HTTP 就是这样的协议。

构建和安装代码包的时候都会执行编译、打包等操作,并且,这些操作生成的任何文件都会先被保存到某个临时的目录中。

使用自定义的代码包导入路径,避免代码包路径变更影响,在该代码包中的库源码文件的包声明语句的右边加入导入注释,像这样:

package semaphore // import "golang.org/x/sync/semaphore"

这个代码包原本的完整导入路径是github.com/golang/sync/semaphore。这与实际存储它的网络地址对应的。而加入导入注释之后,用以下命令即可下载并安装该代码包了:

go get golang.org/x/sync/semaphore

而 Go 语言官网 golang.org 下的路径 /x/sync/semaphore 并不是存放semaphore包的真实地址。我们称之为代码包的自定义导入路径。需要在 golang.org 这个域名背后的服务端程序上,添加一些支持才能使这条命令成功。

源码文件

Go 的源码文件分为命令源码文件、库源码文件和测试源码文件,它们都有着不同的用途和编写规则。

命令源码文件

Go 通过引入 flag 包来专门接收和解析命令参数。

package main
import (
	"flag"
	"fmt"
)
var name string
func init() {
    // 参数一用于存储该命令参数的值的地址
    // 参数二指定该命令参数的名称
    // 参数三默认值
    // 参数四命令参数简短说明
	flag.StringVar(&name, "name", "everyone", "The greeting object.")
}

func main() {
    // 解析命令参数,并把它们的值赋给相应的变量
	flag.Parse()
	fmt.Printf("Hello %s!\n", name)
}
// 执行命令
// go run demo.go -name="Robert"
// 输出: Hello Robert!

可以使用 go run demo.go --help查看指令说明

自定义命令源码文件的参数使用说明

package main

import (
	"flag"
	"fmt"
	"os"
)
var name string
func init() {
    // flag.CommandLine相当于默认情况下的命令参数容器,可以更深层次地定制参数使用说明
	flag.CommandLine = flag.NewFlagSet("", flag.ExitOnError)
    // 使用 Usage 定义参数说明内容
	flag.CommandLine.Usage = func() {
		fmt.Fprintf(os.Stderr, "Usage of %s:\n", "question");
		flag.PrintDefaults()
	}
    flag.StringVar(&name, "name", "everyone", "The greeting object.")
}

func main() {
	flag.Parse()
	fmt.Printf("Hello, %s!\n", name)
}

执行 go run demo.go --help

Usage of question:
 -name string
    The greeting object. (default "everyone")
exit status 2

库源码文件

**库源码文件不能直接运行,仅用于存放程序实体,这些程序实体可以被其他代码使用。**在 Go 语言中,程序实体是变量、常量、函数、结构体和接口的统称。

// lib.go
package main
import "fmt"
func hello(name string) {
	fmt.Printf("Hello, %s!\n", name)
}

声明 demo.go 放在 lib.go 相同目录下。在同一个目录下的源码文件都需要被声明为属于同一个代码包。源码文件声明的包名可以与其所在目录的名称不同,只要这些文件声明的包名一致就可以

// demo.go
package main
func main() {
	hello("Test")
}

代码包声明的基本规则:

  • 同目录下的源码文件的代码包声明语句要一致。
  • 源码文件声明的代码包的名称可以与其所在的目录的名称不同

引入其它的代码包

  • 引入其它的代码包时,只能使用其它代码包中首字母大写的程序实体。Go 通过首字母大小写来代表可见性,大写public/小写private,只有名称的首字母为大写的程序实体才可以被当前包外的代码引用,否则它就只能被当前包内的其他代码引用。
  • 引入其它代码包的源码文件所在的目录相对于 src 目录的相对路径就是它的代码包导入路径,而实际使用其程序实体时给定的限定符要与它声明所属的代码包名称对应,例如 package lib5,则使用其内部程序实体的方法是 lib5.xxx
  • 在 Go 1.5 及后续版本中,我们可以通过创建internal代码包让一些程序实体仅仅能被当前模块中的其他代码引用。这被称为 Go 程序实体的第三种访问权限:模块级私有

变量声明

Go 语言中的程序实体包括变量、常量、函数、结构体和接口。 Go 语言是静态类型的编程语言,所以我们在声明变量或常量的时候,都需要指定它们的类型,或者给予足够的信息,这样才可以让 Go 语言能够推导出它们的类型。

Go 语言变量名由字母、数字、下划线组成,其中首个字符不能为数字。声明变量的一般形式是使用 var 关键字,可一次声明多个变量

var identifier1, identifier2 type // 变量类型都为 type

如果没有声明变量类型, Go 语言会自动推断出变量类型。类型推断可以明显提升程序的灵活性,使得代码重构变得更加容易,同时又不会给代码的维护带来额外负担

var d = true // 变量类型为 bool

当一个变量被声明之后,系统自动赋予它该类型的零值:int 为 0,float 为 0.0,bool 为 false,string 为空字符串,指针为 nil 等。所有的内存在 Go 中都是经过初始化的

此外,还可以用小括号声明变量:

var (
	a1 float32,
    a2 bool,
    a3 [5]bool
)

简短格式

name := value
f, err := os.Open(infile)

简短模式(short variable declaration)有以下限制:

  • 定义变量,同时显式初始化。
  • 不能提供数据类型。
  • 只能用在函数内部。不能在函数外使用。
  • 必须至少声明一个新变量,否则代码将不能编译通过。

在编写ifforswitch语句的时候,我们经常使用简单格式安插初始化子句,用来声明一些临时的变量。

var intVal int 
intVal :=1 // 这时候会产生编译错误,因为 := 是一个声明语句

变量的重声明

通过简短形式,可以对同一个代码块中的变量进行重声明。

在 Go 语言中,代码块一般就是一个由花括号括起来的区域,里面可以包含表达式和语句。Go 语言本身以及我们编写的代码共同形成了一个非常大的代码块,也叫全域代码块。

每个源码文件都是一个代码块,每个函数也是一个代码块,每个if语句、for语句、switch语句和select语句都是一个代码块。甚至,switchselect语句中的case子句也都是独立的代码块。

变量的重声明是对已经声明过的变量再次声明。其前提条件是:

  • 再次声明时赋予的类型必须与其原本的类型相同,否则会产生编译错误。
  • 只能发生在同一个代码块中。
  • 只有在使用简短形式变量声明时才会发生,否则也无法通过编译
  • 被“声明并赋值”的变量必须是多个,并且其中至少有一个是新的变量。
// 变量重声明
var err error
n, err := io.WriteString(os.Stdout, "Hello, everyone!\n")

变量交换的简写代码

var v1 = 10
var v2 = 20
v1, v2 = v2, v1 // 交换变量的值

变量作用域

**一个程序实体的作用域总是会被限制在某个代码块中,而这个作用域最大的用处,就是对程序实体的访问权限的控制。**程序实体的访问权限有三种:包级私有的、模块级私有的和公开的。作用域内声明的变量只能在作用域内使用,如果没有找到变量会往作用域外查找变量。

在具有嵌套关系的不同代码块中存在重名的变量,内部代码块的同名变量会"屏蔽"外部代码块的同名变量。

变量类型

在 Go 编程语言中,数据类型用于声明函数和变量。根据内存大小需要,给不同的变量赋于不同的类型,就可以充分利用内存。Go语言的数据类型有:

类型描述
布尔型值只可以是常量 true 或者 false。如:var b bool = true
数字类型整型 int 和浮点型 float32、float64,Go 语言支持整型和浮点型数字,并且支持复数,其中位的运算采用补码。
字符串类型一串固定长度的字符连接起来的字符序列。字符串是由单个字节连接起来的。Go 语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本。
派生类型包括:指针(Pointer)、数组、 结构化(struct)、Channel 、 函数、切片、 接口类型(interface)、Map 类型

其中,数字类型分有符号位和无符号位:

  • uint8 无符号 8 位整型 (0 到 255)
  • uint16 无符号 16 位整型 (0 到 65535)
  • uint32 无符号 32 位整型 (0 到 4294967295)
  • uint64 无符号 64 位整型 (0 到 18446744073709551615)
  • int8 有符号 8 位整型 (-128 到 127)
  • int16 有符号 16 位整型 (-32768 到 32767)
  • int32 有符号 32 位整型 (-2147483648 到 2147483647)
  • int64 有符号 64 位整型 (-9223372036854775808 到 9223372036854775807)

浮点型有:

  • float32 IEEE-754 32位浮点型数
  • float64 IEEE-754 64位浮点型数
  • complex64 32 位实数和虚数
  • complex128 64 位实数和虚数

其他数字类型:

  • uintptr 无符号整型,用于存放一个指针

字符类型: 字符串中的每一个元素叫做“字符”,在遍历或者单个获取字符串元素时可以获得字符。

  • byte 类似 uint8,代表了 ASCII 码的一个字符。
  • rune 类似 int32,代表一个 UTF-8 字符,当需要处理中文、日文或者其他复合字符时,则需要用到 rune 类型。

类型断言

// 类型断言表达式
value, ok := interface{}(container).([]string)
  • interface{}(container) 作用是把container变量的值转换为空接口值
  • .([]string) 作用是判断变量类型是否为切片类型。
  • 表达式的结果赋给两个变量,变量ok是布尔(bool)类型的,它将代表类型判断的结果,true或false。
    • 如果是true,那么被判断的值将会被自动转换为[]string类型的值,并赋给变量value,否则value将被赋予nil(即“空”)。
    • 可以不用ok变量。但是这样的话,当判断为否时就会引发异常。

类型断言表达式的语法形式是x.(T)。其中的x代表要被判断类型的那个值。这个值当下的类型必须是接口类型的。当变量类型不是任何的接口类型时,需要先把它转成某个接口类型的值。在 Go 语言中,interface{}代表空接口,任何类型都是它的实现类型。(一对不包裹任何东西的花括号,除了可以代表空的代码块之外,还可以用于表示不包含任何内容的数据结构)。

package main
import (
	"fmt"
)
var container = []string{"zero", "one", "two"}
func main() {
	container := map[int]string{0: "zero", 1: "one", 2: "two"}

    // 方式1: 使用断言表达式判断类型
	_, ok1 := interface{}(container).([]string)
	_, ok2 := interface{}(container).(map[int]string)
	if !(ok1 || ok2) {
		fmt.Printf("Error: unsupported container type: %T\n", container)
		return
	}
	fmt.Printf("The element is %q. (container type: %T)\n",
		container[1], container)

    // 方式2: 使用 switch语句判断类型
	elem, err := getElement(container)
	if err != nil {
		fmt.Printf("Error: %s\n", err)
		return
	}
	fmt.Printf("The element is %q. (container type: %T)\n",
		elem, container)
}

func getElement(containerI interface{}) (elem string, err error) {
	switch t := containerI.(type) {
	case []string:
		elem = t[1]
	case map[int]string:
		elem = t[1]
	default:
		err = fmt.Errorf("unsupported container type: %T", containerI)
		return
	}
	return
}
// 执行结果
// The element is "one". (container type: map[int]string)
// The element is "one". (container type: map[int]string)

类型转换

语法形式是T(x)。其中的x可以是一个变量,也可以是一个代表值的字面量(比如1.23struct{}),还可以是一个表达式。在这个上下文中,x可以被叫做源值,它的类型就是源类型,而那个T代表的类型就是目标类型。

  • 对于整数类型值、整数常量之间的类型转换,原则上只要源值在目标类型的可表示范围内就是合法的。

    • 整数在 Go 语言以及计算机中都是以补码的形式存储的。补码就是原码各位求反再加 1。当范围大的数赋值给范围小的数时,整数值的类型的有效范围由宽变窄,在补码形式下截掉一定数量的高位二进制数。
  • 把一个整数值转换为一个string类型的值是可行的,被转换的整数值应该可以代表一个有效的 Unicode 代码点,否则转换的结果将会是"�"(仅由高亮的问号组成的字符串值)。

    • 字符'�'的 Unicode 代码点是U+FFFD。它是 Unicode 标准中定义的 Replacement Character,专用于替换那些未知的、不被认可的以及无法展示的字符。

      fmt.Printf("%s\n", string(-1)) // �
      fmt.Printf("%U\n", '�') // U+FFFD
      
  • 一个值在从string类型向[]byte类型转换时代表着以 UTF-8 编码的字符串会被拆分成零散、独立的字节。

    fmt.Printf(string([]byte{'\xe4', '\xbd', '\xa0', '\xe5', '\xa5', '\xbd'}))
    // 你好
    

    一个值在从string类型向[]rune类型转换时代表着字符串会被拆分成一个个 Unicode 字符。

    fmt.Printf("The string: %q\n", string([]rune{'\u4F60', '\u597D'}))
    // 你好
    

别名类型

可以使用关键字type声明自定义的各种类型。这些类型必须在 Go 语言基本类型和高级类型的范畴之内。

type MyString = string

这条声明语句表示,MyStringstring类型的别名类型。顾名思义,别名类型与其源类型的区别只是在名称上。

Go 语言内建的基本类型中就存在两个别名类型。byteuint8的别名类型,而runeint32的别名类型。

注意

type MyString2 string // 注意,这里没有等号。

MyString2string就是两个不同的类型了。这里的MyString2是一个新的类型,不同于其他任何类型。

这种方式也可以被叫做对类型的再定义。把string类型再定义成了另外一个类型MyString2string可以被称为MyString2的潜在类型。潜在类型的含义是某个类型在本质上是哪个类型或者是哪个类型的集合。潜在类型相同的不同类型的值之间是可以进行类型转换的。

但对于集合类的类型[]MyString2[]string来说这样做却是不合法的,因为[]MyString2[]string的潜在类型不同,分别是MyString2string

另外,即使两个类型的潜在类型相同,它们的值之间也不能进行判等或比较,它们的变量之间也不能赋值。

常量和枚举

Go语言中的常量使用关键字 const 定义,用于存储不会改变的数据,常量是在编译时被创建的,即使定义在函数内部也是如此,并且只能是布尔型、数字型(整数型、浮点型和复数)和字符串型

Go语言的常量有个不同寻常之处:许多常量并没有一个明确的基础类型。编译器为这些没有明确的基础类型的数字常量提供比基础类型更高精度的算术运算,可以认为至少有 256bit 的运算精度。通过延迟明确常量的具体类型,不仅可以提供更高的运算精度,而且可以直接用于更多的表达式而不需要显式的类型转换。

由于编译时的限制,定义常量的表达式必须为能被编译器求值的常量表达式。

const identifier [type] = value
const c_name1, c_name2 = value1, value2 // 同时声明多个常量

常量还可用作枚举

import "unsafe"
const (
    a = "abc"
    b = len(a) // 常量表达式中,函数必须是内置函数,否则编译不过
    c = unsafe.Sizeof(a)
)

iota

常量声明可以使用 iota 常量生成器初始化,它用于生成一组以相似规则初始化的常量,但是不用每行都写一遍初始化表达式。iota 可以认为是一个可以被编译器修改的常量。

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

const (
    a = iota // 0
    b = iota // 1 可省略 ioa
    c = iota // 2 可省略 ioa
)

iota 表示从 0 开始自动加 1,如果遇到手动赋值的加 1 后跳过,例如

package main
import "fmt"
func main() {
    const (
            a = iota   //0
            b          //1
            c = "ha"   //独立值,iota += 1
            d          //"ha"   iota += 1
            e = iota   //4,恢复计数
            f          //5
    )
    fmt.Println(a,b,c,d,e,f)
    // 结果为 0 1 2 ha ha 4 5
}

iota 可以配置 左移 << 、右移 >> 运算符来设置枚举的值。

条件语句

通过指定一个或多个条件,通过条件的结果来决定执行语句

if 布尔表达式 {
   /* 在布尔表达式为 true 时执行 */
} else {
  /* 在布尔表达式为 false 时执行 */
}

Go 的 if 还有一个强大的地方就是条件判断语句里面允许声明一个变量,这个变量的作用域只能在该条件逻辑块内,其他地方就不起作用。

func main() {
    if num := 9; num < 0 {
        fmt.Println(num, "is negative")
    } else if num < 10 {
        fmt.Println(num, "has 1 digit")
    } else {
        fmt.Println(num, "has multiple digits")
    }
}
// 执行结果
// 9 has 1 digit

Go 的条件语句不需使用括号将条件包含起来,但大括号 {} 必须存在,且左括号必须在if或else的同一行。在有返回值的函数中,最终的return不能在条件语句中

switch语句

switch var1 {
    case val1:
        ...
    case val2:
        ...
    default:
        ...
}

Go 的 Switch 语句匹配项后面也不需要再加 break。如果需要执行后面的 case 语句,需要使用 fallthrough

select 语句

select 是 Go 中的一个控制结构,类似于用于通信的 switch 语句。每个 case 必须是一个通信操作,要么是发送要么是接收。select 随机执行一个可运行的 case。如果没有 case 可运行,它将阻塞,直到有 case 可运行。一个默认的子句应该总是可运行的。

select {
    case communication clause  :
       statement(s);      
    case communication clause  :
       statement(s);
    /* 你可以定义任意数量的 case */
    default : /* 可选 */
       statement(s);
}

如果有多个 case 都可以运行,Select 会随机公平地选出一个执行。其他不会执行。否则执行 default 语句。如果没有 default 子句,select 将阻塞,直到某个通信可以运行;Go 不会重新对 channel 或值进行求值。

循环语句

循环语句用于在程序中就需要重复执行某些语句。

for循环

// 形式一: 和 C 语言的 for 一样:
for init; condition; post { }
// 形式二: 和 C 的 while 一样:
for condition { }
// 形式三: 和 C 的 for(;;) 一样:
for { }

无限循环

func main() {
    sum := 0
    for {
        sum++ // 无限循环下去
    }
    fmt.Println(sum) // 无法输出
}

for range 循环

这种格式的循环可以对字符串、数组、切片等进行迭代输出元素。

strings := []string{"google", "runoob"}
for i, s := range strings {
    fmt.Println(i, s)
}
// 执行结果
// 0 google
// 1 runoob

循环控制语句可以控制循环体内语句的执行过程。

控制语句描述
break 语句经常用于中断当前 for 循环或跳出 switch 语句
continue 语句跳过当前循环的剩余语句,然后继续进行下一轮循环。
goto 语句将控制转移到被标记的语句。
func main() {
   /* 定义局部变量 */
   var a int = 13
   /* 循环 */
   LOOP: for a < 17 {
      if a == 15 {
         /* 跳过迭代 */
         a = a + 1
         // 跳回 LOOP 不执行下面的打印语句
         goto LOOP
      }
      fmt.Printf("a的值为 : %d\n", a)
      a++    
   }  
}
// 执行结果
// a的值为 : 13
// a的值为 : 14
// a的值为 : 16

函数

在 Go 语言中,函数可是一等的(first-class)公民,函数类型也是一等的数据类型。函数不但可以用于封装代码、分割功能、解耦逻辑,还可以化身为普通的值,在其他函数间传递、赋予变量、做类型判断和转换等等,就像切片和字典的值那样。函数是基本的代码块,用于执行一个任务。

main() 函数是 Go 语言的入口,一个程序只有一个。函数定义格式:

func function_name( [parameter list] ) [return_types] {
   函数体
}

Go 的函数可以有多个返回值

package main
import "fmt"
func swap(x, y string) (string, string) {
   return y, x
}
func main() {
   a, b := swap("Google", "Runoob")
   fmt.Println(a, b) // 交换位置 Runoob Google
}

函数参数

默认情况下,Go 语言使用的是值传递,即在调用过程中不会影响到实际参数。调用函数,可以通过两种方式来传递参数:

  • 值传递: 指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
  • 引用传递: 指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。引用传递是指在调用函数时将**实际参数的地址(指针)**传递到函数中

函数作为实参

Go 语言支持函数式编程。 在 Go 语言中,函数是第一公民,可以作为参数传递、支持匿名函数和闭包、可以满足接口。常常通过传入函数来实现回调的效果

func main(){
   /* 声明函数变量 */
   getSquareRoot := func(x float64) float64 {
      return math.Sqrt(x)
   }
   /* 使用函数 */
   fmt.Println(getSquareRoot(9))
}
// 执行结果: 3

高阶函数

高阶函数可以满足下面的两个条件:

  1. 接受其他的函数作为参数传入;
  2. 把其他的函数作为结果返回。

只要满足了其中任意一个特点,我们就可以说这个函数是一个高阶函数。高阶函数也是函数式编程中的重要概念和特征。

// 声明函数类型
type operate func(x, y int) int

// 高阶函数: 接受其他的函数作为参数传入
func calculate(x int, y int, op operate) (int, error) {
    // 函数类型属于引用类型,它的值可以为nil,而这种类型的零值恰恰就是nil。
	if op == nil {
		return 0, errors.New("invalid operation")
	}
	return op(x, y), nil
}

// 声明函数类型
type calculateFunc func(x int, y int) (int, error)
// 高阶函数: 把其他的函数作为结果返回。
func genCalculator(op operate) calculateFunc {
	return func(x int, y int) (int, error) {
		if op == nil {
			return 0, errors.New("invalid operation")
		}
		return op(x, y), nil
	}
}

函数闭包

自由变量: 一个函数中存在对外来标识符的引用,该引用就是自由变量。

Go 语言支持匿名函数,可作为闭包。匿名函数是一个"内联"语句或表达式。匿名函数的优越性在于可以直接使用函数内的变量,不必申明。

package main
import "fmt"

func getSequence() func() int {
   i:=0
   return func() int {
      i+=1
     return i  
   }
}

func main(){
   /* nextNumber 为一个函数,函数 i 为 0 */
   nextNumber := getSequence()  
   /* 调用 nextNumber 函数,i 变量自增 1 并返回 */
   fmt.Println(nextNumber())
   
   /* 创建新的函数 nextNumber1,并查看结果 */
   nextNumber1 := getSequence()  
   fmt.Println(nextNumber1())
   fmt.Println(nextNumber1())
   // 执行结果: 1 1 2 
}

指针

Go语言为程序员提供了控制数据结构指针的能力,但是,并不能进行指针运算。Go语言允许你控制特定集合的数据结构、分配的数量以及内存访问模式,这对于构建运行良好的系统是非常重要的。

Go 语言的uintptr类型:该类型实际上是一个数值类型,是 Go 语言内建的数据类型之一。根据当前计算机的计算架构的不同,它可以存储 32 位或 64 位的无符号整数,可以代表任何指针的位(bit)模式,也就是原始的内存地址。

Go 语言的取地址符是 &,放到一个变量前使用就会返回相应变量的内存地址。通过指针变量指向了一个值的内存地址

var ip *int        /* 指向整型*/
var fp *float32    /* 指向浮点型 */

在指针类型前面加上 * 号(前缀)来获取指针所指向的内容

func main() {
   var a int= 20   /* 声明实际变量 */
   var ip *int        /* 声明指针变量 */
   ip = &a  /* 指针变量的存储地址 */
   /* 使用指针访问值 */
   fmt.Printf("*ip 变量的值: %d\n", *ip )
}
// 执行结果
// *ip 变量的值: 20

当一个指针被定义后没有分配到任何变量时,它的值为 nilnil 指针也称为空指针。nil 指代零值或空值。

创建指针的另一种方法

Go语言还提供了另外一种方法来创建指针变量,格式如下:

new(类型)
str := new(string)
*str = "Go语言教程"
fmt.Println(*str)

new() 函数可以创建一个对应类型的指针,创建过程会分配内存,被创建的指针指向默认值。

unsafe

Go 语言标准库中的unsafe包:unsafe包中有一个类型叫做Pointer,代表了“指针”。unsafe.Pointer可以表示任何指向可寻址的值的指针,同时它也是指针值和uintptr值之间的桥梁。也就是说,通过它,我们可以在这两种值之上进行双向的转换。

Go中不可寻址的值

func main() {
	const num = 123
	//_ = &num // 常量不可寻址。
	//_ = &(123) // 基本类型值的字面量不可寻址。

	var str = "abc"
	_ = str
	//_ = &(str[0]) // 对字符串变量的索引结果值不可寻址。
	//_ = &(str[0:2]) // 对字符串变量的切片结果值不可寻址。
	str2 := str[0]
	_ = &str2 // 但这样的寻址就是合法的。

	//_ = &(123 + 456) // 算术操作的结果值不可寻址。
	num2 := 456
	_ = num2
	//_ = &(num + num2) // 算术操作的结果值不可寻址。

	//_ = &([3]int{1, 2, 3}[0]) // 对数组字面量的索引结果值不可寻址。
	//_ = &([3]int{1, 2, 3}[0:2]) // 对数组字面量的切片结果值不可寻址。
	_ = &([]int{1, 2, 3}[0]) // 对切片字面量的索引结果值却是可寻址的。
	//_ = &([]int{1, 2, 3}[0:2]) // 对切片字面量的切片结果值不可寻址。
	//_ = &(map[int]string{1: "a"}[0]) // 对字典字面量的索引结果值不可寻址。

	var map1 = map[int]string{1: "a", 2: "b", 3: "c"}
	_ = map1
	//_ = &(map1[2]) // 对字典变量的索引结果值不可寻址。

	//_ = &(func(x, y int) int {
	//	return x + y
	//}) // 字面量代表的函数不可寻址。
	//_ = &(fmt.Sprintf) // 标识符代表的函数不可寻址。
	//_ = &(fmt.Sprintln("abc")) // 对函数的调用结果值不可寻址。

	dog := Dog{"little pig"}
	_ = dog
	//_ = &(dog.Name) // 标识符代表的函数不可寻址。
	//_ = &(dog.Name()) // 对方法的调用结果值不可寻址。

	//_ = &(Dog{"little pig"}.name) // 结构体字面量的字段不可寻址。

	//_ = &(interface{}(dog)) // 类型转换表达式的结果值不可寻址。
	dogI := interface{}(dog)
	_ = dogI
	//_ = &(dogI.(Named)) // 类型断言表达式的结果值不可寻址。
	named := dogI.(Named)
	_ = named
	//_ = &(named.(Dog)) // 类型断言表达式的结果值不可寻址。

	var chan1 = make(chan int, 1)
	chan1 <- 1
	//_ = &(<-chan1) // 接收表达式的结果值不可寻址。

}

不可寻址的特点:

  • 不可变的。由于 Go 语言中的字符串值也是不可变的,所以对于一个字符串类型的变量来说,基于它的索引或切片的结果值也都是不可寻址的,因为即使拿到了这种值的内存地址也改变不了什么。
  • 临时结果。这个关键词能被用来解释很多现象。我们可以把各种对值字面量施加的表达式的求值结果都看做是临时结果。
  • 不安全的。“不安全的”操作很可能会破坏程序的一致性,引发不可预知的错误,从而严重影响程序的功能和稳定性。

一个需要特别注意的例外是,对切片字面量的索引结果值是可寻址的。因为不论怎样,每个切片值都会持有一个底层数组,而这个底层数组中的每个元素值都是有一个确切的内存地址的。

不可寻址的值在使用上限制

  • 无法使用取址操作符&获取它们的指针了。
  • Go 语言中的++--并不属于操作符,而分别是自增语句和自减语句的重要组成部分。不能用于不可寻址的值,除了字典字面量和字典变量索引表达式的结果值。
  • 在带有range子句的for语句中,在range关键字左边的表达式的结果值也都必须是可寻址的,不过对字典的索引结果值同样可以被用在这里。

通过unsafe.Pointer操纵可寻址的值

unsafe.Pointer可以表示任何指向可寻址的值的指针,它是指针值和uintptr值之间的桥梁。它可以绕过 Go 语言的编译器和其他工具的重重检查,并达到潜入内存修改数据的目的。这并不是一种正常的编程手段,使用它会很危险,很有可能造成安全隐患。

作为上层应用的开发者,请谨慎地使用unsafe包中的任何程序实体。

dog := Dog{"little pig"}
dogP := &dog // 取出指针值
// 使用了两个类型转换,先把dogP转换成了一个unsafe.Pointer类型的值
// 然后紧接着又把后者转换成了一个uintptr的值,并把它赋给了变量dogPtr。
dogPtr := uintptr(unsafe.Pointer(dogP))
  1. 一个指针值(比如*Dog类型的值)可以被转换为一个unsafe.Pointer类型的值,反之亦然。
  2. 一个uintptr类型的值也可以被转换为一个unsafe.Pointer类型的值,反之亦然。
  3. 一个指针值无法被直接转换成一个uintptr类型的值,反过来也是如此。

所以,对于指针值和uintptr类型值之间的转换,必须使用unsafe.Pointer类型的值作为中转。

namePtr := dogPtr + unsafe.Offsetof(dogP.name)
nameP := (*string)(unsafe.Pointer(namePtr))
  • unsafe.Offsetof函数用于获取两个值在内存中的起始存储地址之间的偏移量,以字节为单位。

有了这个偏移量,又有了结构体值在内存中的起始存储地址(这里由dogPtr变量代表),把它们相加我们就可以得到dogPname字段值的起始存储地址了。这个地址由变量namePtr代表。

此后,我们可以再通过两次类型转换把namePtr的值转换成一个*string类型的值,这样就得到了指向dogPname字段值的指针值。

这样,可以通过 namePtr 直接修改埋藏得很深的内部数据。但是,一旦我们有意或无意地把这个内存地址泄露出去,那么其他人就能够肆意地改动dogP.name的值,以及周围的内存地址上存储的任何数据了。

即使他们不知道这些数据的结构也无所谓啊,改不好还改不坏吗?不正确地改动一定会给程序带来不可预知的问题,甚至造成程序崩溃。

参考资料

Go 语言教程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值