10

Golang入门(2):一天学完GO的基本语法

 4 years ago
source link: https://studygolang.com/articles/27809
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

在配置好环境之后,要研究的就是这个语言的语法了。在这篇文章中,作者希望可以简单的介绍一下Golang的各种语法,并与C和Java作一些简单的对比以加深记忆。因为这篇文章只是入门Golang的第二篇文章,所以本文并不会对一些指令进行深挖,仅仅只是停留在“怎么用”的程度,至于“为什么是这样”,则涉及到了具体的应用场景和汇编指令,作者将会在以后的文章中进行介绍。

1 导包

总所周知,“Hello World”是程序员的一种仪式感。

而这一行“Hello World”,一定会涉及到输入输出相关的方法。所以,如何导入包,是我们需要研究的第一步。

在C语言中,我们使用 include ,在Java中,我们使用了 import 。在Golang中也一样,我们使用 import 引入其他的包。在 上一篇文章 中,我们已经提到了对于导入的包,编译器会首先在 GOROOT 中寻找,随后会在项目所对应的 GOPATH 中寻找,最后才是在 全局GOPATH 中寻找,如果都无法找到,编译器将会报错。

注意,在Golang中和Java有一点很大的区别,就是在Golang中,import导入的是目录,而不是包名。而且,Golang没有强制要求包名和目录名需要一致。

下面举一些例子来说明在Golang中包名和目录的关系,先来看看目录结构:

ua6nmeA.png!web

可以看出,我们在 src 下面设置了两个文件夹,在第二个文件夹下面设置了两个go文件。

来看看这两个文件的代码, test1.go

如下:

package pktest

func Func1()  {
	println("这是第一个函数")
}
复制代码

test2.go如下:

package pktest

func Func2()  {
	println("这是第二个函数")
}
复制代码

然后我们再来看看 testmain.go 下面的内容:

package main

import "package1/package2"

func main() {
	pktest.Func1()
}
复制代码

注意到了吗,我们在调用 Func1 这个函数的时候,使用的是 pktest ,而不是我们认为的 package1/package2 中的 package2

按照我们在Java中的思想,我们应该是使用 package2.Func1 的调用方法或者说是使用 test1.Func1 这样的方法。

这是因为在Golang中, 没有强制要求包名和目录名称一致 。也就是说,在上面的例子中,我们引用路径中的文件夹名称是 package2 ,而在这个文件夹下面的两个文件,他们的包名,却被设置成了 pktest 。而在Golang的引用中,我们需要填写的是 源文件所在的相对路径

也就是说,我们可以理解为,包名和路径其实是两个概念,文件名在Golang中不会被显式的引用,通常的引用格式是 packageName.FunctionName

结论如下:

import

以上部分内容摘自于 这篇文章

2 声明

看完了导包方面的内容,我们再来看看如何声明一个变量。在声明变量这一部分,和C以及Java也有 较大的区别

2.1 变量的定义

我们先定义一些变量看看:

var a int
var b float32
var c, d float64
e, f := 9, 10
var g = "Ricardo"
复制代码

我们可以看到,在Golang中定义一个变量,需要使用 var 关键字,而与C或者Java不同的是,我们需要将这个变量的类型写在变量名的 后面 。不仅如此,在Golang中,允许我们一次性定义多个变量并同时赋值。

还有另外的一种做法,是使用 := 这个符号。使用了这个符号之后,开发者不再需要写 var 关键字,只需要定义变量名,并在后面进行赋值即可。并且,Golang编译器会根据后面的值的类型,自动推导出变量的类型。

在变量的定义过程中,如果定义的时候就赋予了变量的初始值,是不需要再声明变量的类型的,如变量 g

注意,Golang是强类型的一种语言,所有的变量必须拥有类型,并且变量仅仅可以存储特定类型的数据。

2.2 匿名变量

标识符为_(下划线)的变量,是系统保留的匿名变量,在赋值后,会被立即释放,称之为匿名变量。其作用是变量占位符,对其变量赋值结构。通常会在批量赋值时使用。

例如,函数返回多个值,我们仅仅需要 其中部分 ,则不需要的使用_来占位

func main() {
  // 调用函数,仅仅需要第二个返回值,第一,三使用匿名变量占位
  _, v, _ := getData()
  fmt.Println(v)
}
// 返回两个值的函数
func getData() (int, int, int) {
  // 返回3个值
  return 2, 4, 8
}
复制代码

如上述代码所示,如果我仅仅需要一个变量的值,就不需要去额外定义一些没有意义的变量名了,仅仅只是需要使用占位符这种“用后即焚”的匿名变量。

2.3 常量

在Golang的常量定义中,使用 const 关键字,并且 不能 使用 := 标识符。

3 判断

我们在使用Java或者C的时候,写判断语句是这样的:

if(condition){
    ...
}
复制代码

在Golang中,唯一的不同是不需要小括号,但是大括号还是必须的。如下:

func pow(x, n, lim float64) float64 {
	if v := math.Pow(x, n); v < lim {
	    return v
	}
	return lim
}
复制代码

除去不需要写小括号以外,Golang还允许在判断条件之前执行一个简单的语句,并用一个分号 隔开。

4 循环

在Golang中,只有一种循环, for 循环。

和判断语句一样,在Golang中也是没有小括号的。

func main() {
	sum := 0
	for i := 0; i < 10; i++ {
		sum += i
	}
	fmt.Println(sum)
}
复制代码

此外,在循环条件中,初始化语句和后置语句是可选的,这个时候把分号去掉, for循环 就变成了 while循环

func main() {
	sum := 1
	for sum < 1000 {
		sum += sum
	}
	fmt.Println(sum)
}
复制代码

不仅如此,如果省略循环条件,该循环就不会结束,因此无限循环可以写得很紧凑,这个时候,和 while(true) 的效果是一样的。

func main() {
	for {
	    ...
	}
}
复制代码

5 函数

5.1 函数的定义

在Golang的函数定义中,所有的函数都以 func 开头,并且Golang命名推荐使用驼峰命名法。

注意,在Golang的函数中,如果首字母是小写,则只能在包内使用;如果首字母是大写,则可以在包外被引入使用。可以理解为,使用小写的函数,是`private`的,使用大写的函数,是`public`的。

在Golang的函数定义中,一样可以不接受参数,或者接受多个参数。而在参数的定义过程中,也是按照定义变量的格式,先定义变量名,再声明变量类型。对于函数的返回类型,也是按照这样的格式,先写函数名,再写返回类型:

func add(x int, y int) int {
	return x + y
}

func main() {
	fmt.Println(add(42, 13))
}
复制代码

并且,对于相同类型的两个参数,参数类型可以只写一个,用法如下:

func add(x, y int) int {
	return x + y
}
复制代码

在Golang中,对于函数的返回值,和C以及Java是不一样的。

Golang中的函数可以返回任意多个返回值。

例如下面的小李子,

func swap(x, y string) (string, string) {
	return y, x
}

func main() {
	a, b := swap("hello", "world")
	fmt.Println(a, b)
}
复制代码

其次,函数的返回值是可以被命名的:

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

在这里,我们可以理解为在函数的顶部预先定义了这些变量值,而空的 return 语句则默认返回所有已经定义的返回变量。

5.2defer

在Golang中,有一个关键字叫 defer

defer 语句会将函数推迟到外层函数返回之后执行。 推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。

func main() {
	defer fmt.Println("world")

	fmt.Println("hello")
}
复制代码

在这段代码中,本来的执行路径是从上往下,也就是先输出“world”,然后再输出“hello”。但是因为 defer 这个关键字的存在,这行语句将在最后才执行,所以产生了先打印“hello”然后再打印“world”的效果。

注意,defer后面必须是函数调用语句,不能是其他语句,否则编译器会报错。

可以考虑到的场景是,文件的关闭,或数据库连接的释放等,这样打开和关闭的代码写在一起,既可以使得代码更加的整洁,也可以防止出现开发者在写了长长的业务代码后,忘记关闭的情况。

至于defer的底层实现,本文不进行详细的解释,简单来讲就是将defer语句后面的函数调用的地址压进一个栈中,在当前的函数执行完毕,CPU即将执行函数外的下一行代码之前,先把栈中的指令地址弹出给CPU执行,直到栈为空,才结束这个函数,继续执行后面的代码。

从上文刚刚的表述中也可以推断出,如果有多条refer语句,将会从下往上依次执行。

因为本文只是对各种指令简单的进行对比,所以对于refer的详细解释,将在以后的文章中详细说明。

6 指针

对于指针,如果是C或者C++开发者,一定很熟悉;而对于Java开发者,指针是对开发者透明的一个东西,一个对象会在堆中占据一定的内存空间,而在当前的栈桢中,有一个局部变量,他的值就是那个对象的首地址,这也是一个指针。

可以说,指针就是开发者访问内存的一种途径,只不过是由控制权交给了开发者还是虚拟机。

在Golang中,指针的用法和 C 是一样的。同样是用 & 取地址,用 * 取地址中的值。

但是,与 C 不同,Golang没有指针运算。

7 数组

在Golang中,数组的定义是这样的:

var a [10]int
复制代码

这样做会将变量 a 声明为拥有 10 个整数的数组。

注意,在Golang中,数组的大小也同样和 C 语言一样不能改变。

7.1切片

数组的切片,顾名思义,就是将一个数组按需切出自己所需的部分。

每个数组的大小都是固定的。而切片则为数组元素提供动态大小的、灵活的视角。在实践中,切片比数组更常用。

切片通过两个下标来界定,即一个上界和一个下界,二者以冒号分隔:

a[low : high]
复制代码

它会选择一个半开区间,包括第一个元素,但排除最后一个元素。

以下表达式创建了一个切片,它包含 a 中下标从 1 到 3 的元素:

a[1:4]
复制代码

举个例子:

func main() {
	str := [4]string{
	    "aaa",
	    "bbb",
	    "ccc",
	    "ddd",
	}
	fmt.Println(str)

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

	b[0] = "XXX"
	fmt.Println(a, b)
	fmt.Println(str)
}
复制代码

我们定义了一个数组,里面含有"aaa","bbb","ccc","ddd"四个元素。然后我们定义了两个切片, ab ,根据定义可以知道, a 为"aaa"和"bbb", b 为"bbb"和"ccc"。

这个时候,我们把b[0]改成了"XXX",那么 b 变成了"XXX"和"ccc",这是毋庸置疑的。但是与直觉相违背的是,这个时候的数组 str ,也变成了"aaa","XXX","ccc","ddd"。

这是因为,Golang中的切片,不是拷贝,而是定义了新的指针,指向了原来数组所在的内存空间。所以,修改了切片数组的值,也就相应的修改了原数组的值了。

此外,切片可以用append增加元素。但是,如果此时底层数组容量不够,此时切片将会指向一个重新分配空间后进行拷贝的数组。

因此可以得出结论:

  • 切片并不存储任何数据,它只是描述了底层数组中的一段。
  • 更改切片的元素会修改其底层数组中对应的元素。
  • 与它共享底层数组的切片都会观测到这些修改。

7.2 make

切片可以用内建函数 make 来创建,这也是你创建动态数组的方式。

在此之前需要解释两个定义,len(长度)和cap(容量)。
len是数组的长度,指的是这个数组在定义的时候,所约定的长度。  
cap是数组的容量,指的是底层数组的长度,也可以说是原数组在内存中的长度。
在前文中所提到的切片,如果我定义了一个str[0,0]的切片,此时的长度为0,但是容量依旧还是5。
复制代码

make 函数会分配一个元素为零值的数组并返回一个引用了它的切片:

a := make([]int, 5)  // len(a)=5
复制代码

要指定它的容量,需向 make 传入第三个参数:

b := make([]int, 0, 5) // len(b)=0, cap(b)=5

b = b[:cap(b)] // len(b)=5, cap(b)=5
b = b[1:]      // len(b)=4, cap(b)=4
复制代码

也就是说,make函数可以自定义切片的大小。用Java的话来说,他可以被重载。

有两种形式,如果只有两个参数,第一个参数是数组内元素的类型,第二个参数是数组的长度(此时长度和容量都为5)。

而如果有第三个参数,那么第三个参数可以指定数组的容量,即可以指定这个数组在内存中分配多大的空间。

写在最后

首先,谢谢你能看到这里。

如果这篇文章对你能起到哪怕一点点的帮助,作者都会很开心!

其次要说明的是,作者也是刚开始接触Golang,写这篇文章的目的是起到一个笔记的效果,能够去比较一些C,Java,Golang中的语法区别,也一定会有不少的认知错误。如果在这篇文章中你看到了任何与你的认识有差距的地方,请一定指出作者的错误。如果本文有哪些地方是作者讲的不够明白的,或者是你不理解的,也同样欢迎留言,一起交流学习进步。

而且在本文中,很多地方没有进行深入挖掘,这些作者都有记录,并且打算在之后的文章中,也会从源码的角度出发,分析这些原因。在这篇文章中,就只是单纯的学会怎么用,就达到目的了。

那么在最后,再次感谢~


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK