Mark blog

知行合一 划水归档

Go 语言简介(二)

内存分配

new 是一个分配内存的内建函数,但不同于其他语言中同名的new所作的工作,它只是将内存清零,而不是初始化内存。new(T)为一个类型为T的新项目分配了值为零的存储空间并返回其地址,也就是一个类型为T的值。用Go的术语来说,就是*它返回了一个指向新分配的类型为T的零值的指针**。

**make**(T,args)函数的目的与new(T)不同。它仅用于创建切片、map和chan(消息管道),并返回类型T(不是*T)的一个被初始化了的(不是)实例。这种差别的出现是由于这三种类型实质上是对在使用前必须进行初始化的数据结构的引用。例如,切片是一个具有三项内容的描述符,包括指向数据(在一个数组内部)的指针、长度以及容量,在这三项内容被初始化之前,切片值为nil。对于切片、映射和信道,make初始化了其内部的数据结构并准备了将要使用的值。如:

下面的代码分配了一个整型数组,长度为10,容量为100,并返回前10个数组的切片

1
make([]int, 10, 100)

以下示例说明了newmake的不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
var p *[]int = new([]int)   // 为切片结构分配内存;*p == nil;很少使用
var v []int = make([]int, 10) // 切片v现在是对一个新的有10个整数的数组的引用

// 不必要地使问题复杂化:
var p *[]int = new([]int)
fmt.Println(p) //输出:&[]
*p = make([]int, 10, 10)
fmt.Println(p) //输出:&[0 0 0 0 0 0 0 0 0 0]
fmt.Println((*p)[2]) //输出: 0

// 习惯用法:
v := make([]int, 10)
fmt.Println(v) //输出:[0 0 0 0 0 0 0 0 0 0]

函数

要注意Go语言这种声明变量类型和函数返回值是反过来的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main
import "fmt"

func max(a int, b int) int { //注意参数和返回值是怎么声明的

if a > b {
return a
}
return b
}

func main(){
fmt.Println(max(4, 5))
}

函数返回多个值

Go中很多Package 都会返回两个值,一个是正常值,一个是错误,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main
import "fmt"

func main(){
v, e := multi_ret("one")
fmt.Println(v,e) //输出 1 true

v, e = multi_ret("four")
fmt.Println(v,e) //输出 0 false

//通常的用法(注意分号后有e)
if v, e = multi_ret("four"); e {
// 正常返回
}else{
// 出错返回
}
}

func multi_ret(key string) (int, bool){
m := map[string]int{"one": 1, "two": 2, "three": 3}

var err bool
var val int

val, err = m[key]

return val, err
}

函数不定参数

例子很清楚了,我就不多说了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func sum(nums ...int) {
fmt.Print(nums, " ") //输出如 [1, 2, 3] 之类的数组
total := 0
for _, num := range nums { //要的是值而不是下标
total += num
}
fmt.Println(total)
}
func main() {
sum(1, 2)
sum(1, 2, 3)

//传数组
nums := []int{1, 2, 3, 4}
sum(nums...)
}

函数闭包

nextNum这个函数返回了一个匿名函数,这个匿名函数记住了nextNum中i+j的值,并改变了i,j的值,于是形成了一个闭包的用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func nextNum() func() int {
i,j := 1,1
return func() int {
var tmp = i+j
i, j = j, tmp
return tmp
}
}
//main函数中是对nextNum的调用,其主要是打出下一个斐波拉契数
func main(){
nextNumFunc := nextNum()
for i:=0; i<10; i++ {
fmt.Println(nextNumFunc())
}
}

函数的递归

和c基本是一样的

1
2
3
4
5
6
7
8
9
10
func fact(n int) int {
if n == 0 {
return 1
}
return n * fact(n-1)
}

func main() {
fmt.Println(fact(7))
}

结构体

Go的结构体和C的基本上一样,不过在初始化时有些不一样,Go支持带名字的初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type Person struct {
name string
age int
email string
}

func main() {
//初始化
person := Person{"Tom", 30, "tom@gmail.com"}
person = Person{name:"Tom", age: 30, email:"tom@gmail.com"}

fmt.Println(person) //输出 {Tom 30 tom@gmail.com}

pPerson := &person

fmt.Println(pPerson) //输出 &{Tom 30 tom@gmail.com}

pPerson.age = 40
person.name = "Jerry"
fmt.Println(person) //输出 {Jerry 40 tom@gmail.com}
}

结构体方法

注意:Go语言中没有public, protected, private的关键字,所以,如果想让一个方法可以被别的包访问的话,需要把这个方法的第一个字母大写。这是一种约定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type rect struct {
width, height int
}

func (r *rect) area() int { //求面积
return r.width * r.height
}

func (r *rect) perimeter() int{ //求周长
return 2*(r.width + r.height)
}

func main() {
r := rect{width: 10, height: 15}

fmt.Println("面积: ", r.area())
fmt.Println("周长: ", r.perimeter())

rp := &r
fmt.Println("面积: ", rp.area())
fmt.Println("周长: ", rp.perimeter())
}

接口和多态

接口意味着多态,下面是一个经典的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
//---------- 接 口 --------//
type shape interface {
area() float64 //计算面积
perimeter() float64 //计算周长
}

//--------- 长方形 ----------//
type rect struct {
width, height float64
}

func (r *rect) area() float64 { //面积
return r.width * r.height
}

func (r *rect) perimeter() float64 { //周长
return 2*(r.width + r.height)
}

//----------- 圆 形 ----------//
type circle struct {
radius float64
}

func (c *circle) area() float64 { //面积
return math.Pi * c.radius * c.radius
}

func (c *circle) perimeter() float64 { //周长
return 2 * math.Pi * c.radius
}

// ----------- 接口的使用 -----------//
func interface_test() {
r := rect {width:2.9, height:4.8}
c := circle {radius:4.3}

s := []shape{&r, &c} //通过指针实现

for _, sh := range s {
fmt.Println(sh)
fmt.Println(sh.area())
fmt.Println(sh.perimeter())
}
}

错误处理 - Error接口

函数错误返回可能是C/C++时最让人纠结的东西的,Go的多值返回可以让我们更容易的返回错误,其可以在返回一个常规的返回值之外,还能轻易地返回一个详细的错误描述。通常情况下,错误的类型是error,它有一个内建的接口

1
2
3
type error interface {
Error() string
}

来看一个错误处理的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package main

import "fmt"
import "errors"

//自定义的出错结构
type myError struct {
arg int
errMsg string
}
//实现Error接口
func (e *myError) Error() string {
return fmt.Sprintf("%d - %s", e.arg, e.errMsg)
}

//两种出错
func error_test(arg int) (int, error) {
if arg < 0 {
return -1, errors.New("Bad Arguments - negtive!")
}else if arg >256 {
return -1, &myError{arg, "Bad Arguments - too large!"}
}
return arg*arg, nil
}

//相关的测试
func main() {
for _, i := range []int{-1, 4, 1000} {
if r, e := error_test(i); e != nil {
fmt.Println("failed:", e)
} else {
fmt.Println("success:", r)
}
}
}

程序运行后输出:

1
2
3
failed: Bad Arguments - negtive!
success: 16
failed: 1000 - Bad Arguments - too large!

错误处理-Defer

下面的程序对于每一个熟悉C语言的人来说都不陌生(有资源泄露的问题),C++使用RAII来解决这种问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func CopyFile(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != nil {
return
}

dst, err := os.Create(dstName)
if err != nil {
return
}

written, err = io.Copy(dst, src)
dst.Close()
src.Close()
return
}

Go语言引入了Defer来确保那些被打开的文件能被关闭。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func CopyFile(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != nil {
return
}
defer src.Close()

dst, err := os.Create(dstName)
if err != nil {
return
}
defer dst.Close()

return io.Copy(dst, src)
}

Go的defer语句预设一个函数调用(延期的函数),该调用在函数执行defer返回时立刻运行。该方法显得不同常规,但却是处理上述情况很有效,无论函数怎样返回,都必须进行资源释放。

来看一个defer函数的示例:

1
2
3
for i := 0; i < 5; i++ {
defer fmt.Printf("%d ", i)
}

被延期的函数以后进先出(LIFO)的顺行执行,因此以上代码在返回时将打印4 3 2 1 0。

错误处理-Panic/Recover

对于不可恢复的错误,Go提供了一个内建的panic函数,它将创建一个运行时错误并使程序停止(相当暴力)。该函数接收一个任意类型(往往是字符串)作为程序死亡时要打印的东西。当编译器在函数的结尾处检查到一个panic时,就会停止进行常规的return语句检查。

下面的仅仅是一个示例。实际的库函数应避免panic。如果问题可以容忍,最好是让事情继续下去而不是终止整个程序。

1
2
3
4
5
6
7
var user = os.Getenv("USER")

func init() {
if user == "" {
panic("no value for $USER")
}
}

当panic被调用时,它将立即停止当前函数的执行并开始逐级解开函数堆栈,同时运行所有被defer的函数。如果这种解开达到堆栈的顶端,程序就死亡了。但是,也可以使用内建的recover函数来重新获得Go程的控制权并恢复正常的执行。 对recover的调用会通知解开堆栈并返回传递到panic的参量。由于仅在解开期间运行的代码处在被defer的函数之内,recover仅在被延期的函数内部才是有用的。

你可以简单地理解为recover就是用来捕捉Painc的,防止程序一下子就挂掉了。

下面是一个很简单的例程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func g(i int) {
if i>1 {
fmt.Println("Panic!")
panic(fmt.Sprintf("%v", i))
}

}

func f() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered in f", r)
}
}()

for i := 0; i < 4; i++ {
fmt.Println("Calling g with ", i)
g(i)
fmt.Println("Returned normally from g.")
}
}

func main() {
f()
fmt.Println("Returned normally from f.")
}

运行结果如下:(我们可以看到Painc后的for循环就没有往下执行了,但是main的程序还在往下走)

1
2
3
4
5
6
7
8
Calling g with  0
Returned normally from g.
Calling g with 1
Returned normally from g.
Calling g with 2
Panic!
Recovered in f 2
Returned normally from f.

参考自:

GO 语言简介(上)— 语法

https://coolshell.cn/articles/8460.html