go入门及简单应用

输出

1
2
3
func main() {
fmt.Println("Hello, 世界")
}

随机数

这里通过运行代码会发现生成的随机数总是一样的

1
2
3
func main() {
fmt.Println("My favorite number is", rand.Intn(10))
}

rand包实现的是伪随机数生成器,需要先对随机数种子进行初始化,为了保证每次运行的随机性,通常在对于种子的sourse初始化用时间戳

1
rand.Seed(time.Now().UnixNano()) //随机数种子

rand.Intn()代表的是rand的取值范围

下面是一个随机数的猜数字游戏

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
func main() {
maxNum := 100
rand.Seed(time.Now().UnixNano()) //随机数种子
se := rand.Intn(maxNum)
fmt.Println(se)

fmt.Println("输入数字")
reader := bufio.NewReader(os.Stdin)
for {
input, err := reader.ReadString('\n') // 读取一行输入
if err != nil {
fmt.Println(err)
return
continue
}
//input = strings.TrimSuffix(input, "\n") // 去掉换行符
input = strings.TrimSpace(input) // 这个是去除字符串前后的空白字符 上面那个只能去除换行符 但是输入的时候会敲回车 导致/r 的出现 会报错

guess, err := strconv.Atoi(input) //转换字符串成数字

if err != nil {
fmt.Println(err)
return
continue
}

fmt.Println("ur guess is", guess)

if guess > se {
fmt.Println("large")
} else if guess < se {
fmt.Println("small")
} else {
fmt.Println("corret !")
break
}
}

函数

函数可以没有参数或者接受多个参数,同时在go语言里面函数可以同时返回多个值

接受参数

1
2
3
4
5
6
7
func add(x int, y int) int {
return x + y
}

func main() {
fmt.Println(add(42, 13))
}

注意返回类型以及变量类型后置的写法

有多个类型同时赋值的时候,可以这样写

1
x int, y int -> x, y int

返回参数

函数可以返回任意数量的值,可以这样定义

1
2
3
func swap(x, y string) (string, string) {
return y, x
}

函数命名

  • 函数名称命名

函数名称小写代表函数私有,大写代表函数公有,公有的时候,可以被别的函数跨包调用

  • 函数返回值命名
1
2
3
4
5
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return
}

没有参数的return会直接返回,仅用于短的函数,长的函数会影响代码的可读性

变量

变量的初始化

在这个代码中展示了两种变量赋值的方法,就是创建和赋值,以及创建并赋值

注意go里面变量创建就必须要赋值并使用,否则会报错

1
2
3
4
5
var x, y int
x = 1
y = 2

z := 3

类型转换

go里面类型转换需要显式转换

1
var f float64 = math.Sqrt(float64(x*x + y*y))

常量

用const关键字

1
const Pi = 3.14

循环

for循环

  • 一般for结构
    go里面只有for循环结构,下面是一个例子
1
2
3
for i := 0; i < 10; i++ {
sum += i
}

go语言中没有条件括号,但是必须有花括号,其他的用法与cpp相同

  • for的缺省结构

可以用这种写法替代while

1
2
3
4
sum := 1
for sum < 1000 {
sum += sum
}

判断

if

Go 的 if 语句与 for 循环类似,表达式外无需小括号 ( ) ,而大括号 { } 则是必须的。

1
2
3
if x < 0 {
return sqrt(-x) + "i"
}
  • if 的简短语句

通常可以在if的条件判断语句执行之前,执行一段简短的语句,该语句声明的变量作用域仅在 if 之内。

1
2
3
if value, err = judge(key); err!=nil {
fmt.Error("%v", err)
}
  • if-else语句

语法与c++, java类似

swich

go中的switch语句不限制参数的类型,因此也可以替代if,以及if else语句,

同时switch中不需要加break因为go中执行了相应的case之后会跳出switch

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
fmt.Print("Go runs on ")
switch os := runtime.GOOS; os {
case "darwin":
fmt.Println("OS X.")
case "linux":
fmt.Println("Linux.")
default:
// freebsd, openbsd,
// plan9, windows...
fmt.Printf("%s.\n", os)
}
}

注意 switch中代替if的写法,不带switch

1
2
3
4
5
6
7
8
9
10
11
func main() {
t := time.Now()
switch {
case t.Hour() < 12:
fmt.Println("Good morning!")
case t.Hour() < 17:
fmt.Println("Good afternoon.")
default:
fmt.Println("Good evening.")
}
}

defer

defer 语句会将函数推迟到外层函数返回之后执行。

推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。

这是一个栈式调用,通常用于关闭流

1
2
3
4
5
func main() {
defer fmt.Println("world")

fmt.Println("hello")
}

指针

Go 拥有指针。指针保存了值的内存地址。

go的指针定义与cpp非常相似

类型 *T 是指向 T 类型值的指针。其零值为 nil。

1
var p *int

& 操作符会生成一个指向其操作数的指针。

1
2
i := 42
p = &i
  • 操作符表示指针指向的底层值。
1
2
fmt.Println(*p) // 通过指针 p 读取 i
*p = 21 // 通过指针 p 设置 i

这也就是通常所说的“间接引用”或“重定向”。

与 C 不同,Go 没有指针运算。

结构体

定义

结构体与java类比 有点类似于实体类与对象

下面是一个简单例子

1
2
3
4
5
6
7
8
9
10
type Vertex struct {
X int
Y int
}

func main() {
v := Vertex{1, 2} //定义实例
v.X = 4 //访问字段
fmt.Println(v.X)
}

结构体指针

在这一段代码中,我之前的疑惑时p时结构体v的指针,

如果要访问v的内存,按照c++的写法时需要*p来访问,但是这么写会报错

查询资料后,发现,go语言中对于结构体指针的使用,允许隐式调用,也就是p.x等价于*p.x

原因是go中没有指针运算,一直写*p.x过于啰嗦

1
2
3
4
5
6
7
8
9
10
11
type Vertex struct {
X int
Y int
}

func main() {
v := Vertex{1, 2}
p := &v
p.X = 1e9
fmt.Println(v)
}

结构体指针的实际应用

在函数中对于大的结构体进行调用的时候,直接用应用类型,会导致执行过程中会对结构体进行复制

非常消耗内存,此时可以用结构体指针进行传参,这样可以节省性能消耗

结构体实例化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Vertex struct {
X, Y int
}

var (
v1 = Vertex{1, 2} // 创建一个 Vertex 类型的结构体
v2 = Vertex{X: 1} // Y:0 被隐式地赋予
v3 = Vertex{} // X:0 Y:0
p = &Vertex{1, 2} // 创建一个 *Vertex 类型的结构体(指针)
)

func main() {
fmt.Println(v1, p, v2, v3)
}

运行结果:

{1 2} &{1 2} {1 0} {0 0}

结构体方法

由于go中没有类,但是可以写结构体方法,未结构体加上方法

方法就是一类带特殊的 接收者 参数的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Vertex struct {
X, Y float64
}

func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
} // 修改副本 不改变原址 不常用


func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
} // 指针接收者 可以直接修改实例的值 更常用

而以指针为接收者的方法被调用时,接收者既能为值又能为指针:

1
2
3
4
var v Vertex
v.Scale(5) // OK
p := &v
p.Scale(10) // OK
  • 选择值或指针作为接收者

使用指针接收者的原因有二:

首先,方法能够修改其接收者指向的值。

其次,这样可以避免在每次调用方法时复制该值。若值的类型为大型结构体时,这样做会更加高效。

在本例中,Scale 和 Abs 接收者的类型为 *Vertex,即便 Abs 并不需要修改其接收者。

通常来说,所有给定类型的方法都应该有值或指针接收者,但并不应该二者混用。

所以对于结构体方法的调用 我们通常先取地址 然后才调用函数

1
2
3
4
5
6
7
8
9
10
11
12
type Vertex struct {
X, Y float64
}

func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}

func (v *Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

数组

类型后置,其余一样

切片

定义

切片与数组相比,长度更加灵活,不受限制

切片并不存储任何数据,它只是描述了底层数组中的一段。

更改切片的元素会修改其底层数组中对应的元素。

与它共享底层数组的切片都会观测到这些修改,共享内存

1
2
a := [4]int{1, 2, 3 ,4}
a[low, high]

左闭右开

切片写法

下面这样则会创建一个和上面相同的数组,然后构建一个引用了它的切片:

1
[]bool{true, true, false}

切片默认行为

在进行切片时,你可以利用它的默认行为来忽略上下界。

切片下界的默认值为 0,上界则是该切片的长度。

对于数组

1
var a [10]int

来说,以下切片是等价的:

1
2
3
4
a[0:10]
a[:10]
a[0:]
a[:]

切片与make实现动态数组

1
a := make([]int, 5)  // len(a)=5

append() 向切片追加元素

使用append追加元素的时候,如果追加元素之后,超过了元素的长度,会将切片重新分配地址

由于我们常用空切片 ,因此在追加元素之后都会把切片赋值回去

1
2
3
4
5
6
7
8
9
10
11
// 添加一个空切片
s = append(s, 0)
printSlice(s)

// 这个切片会按需增长
s = append(s, 1)
printSlice(s)

// 可以一次性添加多个元素
s = append(s, 2, 3, 4)
printSlice(s)

map

定义

1
2
3
4
5
6
map[key]value

var m = map[string]int{
"a":1,
"b":2
}

创建

1
2
3
4
5
6
7
8
9
10
11
12
type Vertex struct {
Lat, Long float64
}

var m = map[string]Vertex{
"Bell Labs": Vertex{
40.68433, -74.39967,
},
"Google": Vertex{
37.42202, -122.08408,
},
}

这里的Vertex也可以省略

1
2
3
4
5
6
7
8
type Vertex struct {
Lat, Long float64
}

var m = map[string]Vertex{
"Bell Labs": {40.68433, -74.39967},
"Google": {37.42202, -122.08408},
}

修改map

在映射 m 中插入或修改元素:

1
2
m[key] = elem

获取元素:

1
elem = m[key]

删除元素:

1
delete(m, key)

通过双赋值检测某个键是否存在:

1
elem, ok = m[key]

若 key 在 m 中,ok 为 true ;否则,ok 为 false。

若 key 不在映射中,那么 elem 是该映射元素类型的零值。

同样的,当从映射中读取某个不存在的键时,结果是映射的元素类型的零值。

注 :若 elem 或 ok 还未声明,你可以使用短变量声明:

1
elem, ok := m[key]

range遍历

  • 遍历数组

返回两个参数,第一个参数是索引,第二个参数是value,如果不想要索引,可以用_替代

1
2
3
4
5
6
7
var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}

func main() {
for i, v := range pow {
fmt.Printf("2**%d = %d\n", i, v)
}
}
  • 遍历map
1
2
3
for key, value := range mapName {
xxxx
}

接口

接口类型 是由一组方法签名定义的集合。

1
2
3
type Abser interface {
Abs() float64
}

接口的实现

go中分为显式实现与隐式实现,而隐式实现更为常见

首先定义接口

1
2
3
type sleeper interface {
sleep()
}

然后定义两个结构体

1
2
3
4
5
6
7
type dog struct {
name string
}

type cat struct {
name string
}

定义结构体实现的接口的方法

其实就是把名字省去,然后名字一样的方法

1
2
3
4
5
6
7
func (d *dog) sleep() {
fmt.Printf("dog %s is sleeping", d.name)
}

func (c *cat) sleep() {
fmt.Printf("cat %s is sleeping", c.name)
}

接口同样是一种数据类型 类似于 java 的声明

通过对接口的赋值(实现了接口的结构体)

通过调用接口的方法 实现 Java 里面的上转型

1
2
3
4
5
6
7
8
9
func main() {
var s sleeper
dog := dog{"dd"}
cat := cat{"cc"}
s = &dog
s.sleep() // 实现多态 go会根据类型选择调用哪一个结构体的方法
s = &cat
s.sleep() // 实现多态 go会根据类型选择调用哪一个结构体的方法
}
  • 还可以调用接口切片,然后循环赋值,调用方法
1
2
3
4
5
6
7
func main() {
food := "apple"
for _, i := range []lazy{&dog{"dd"}, &cat{"cc"}} {
i.eat(food)
i.sleep()
}
}

接口嵌套

首先定义接口

1
2
3
4
5
6
7
8
9
10
11
12
type sleeper interface {
sleep()
}

type eater interface {
eat(eat string)
}

type lazy interface {
sleeper
eater
}

然后实现方法

注意 结构体需要实现所有的方法才能够实现该接口 才能进行赋值

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

type cat struct {
name string
}

func (d *dog) sleep() {
fmt.Printf("dog %s is sleeping", d.name)
}
func (d *dog) eat(food string) {
fmt.Println(food + d.name)
}

func (c *cat) sleep() {
fmt.Printf("cat %s is sleeping", c.name)
}
func (c *cat) eat(food string) {
fmt.Println(food + c.name)
}

最后用切片 遍历赋值接口 实现多态

1
2
3
4
5
6
7
func main() {
food := "apple"
for _, i := range []lazy{&dog{"dd"}, &cat{"cc"}} {
i.eat(food)
i.sleep()
}
}

接口断言-检查接口被赋值的是哪个结构体

获取接口值得真正类型

1
2
3
4
dog, ok = s.(dog)
if ok {
fmt.println("is dog")
}

泛型

泛型是通过空接口实现的,所有的结构体都实现了空接口,因此可以通过对空接口进行赋值

空接口可以被任何的结构体赋值 所以这里就实现了泛型

1
var T interface{} // 空接口

fmt包就使用了泛型