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

摘要

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

1 导包

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

而这一行“Hello World”,必定会涉及到输入输出相关的方法。因此,如何导入包,是咱们须要研究的第一步。程序员

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

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

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

项目目录结构

能够看出,咱们在 src下面设置了两个文件夹,在第二个文件夹下面设置了两个go文件。
来看看这两个文件的代码, test1.go以下:

package pktest

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

test2.go以下:app

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这样的方法。ui

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

也就是说,咱们能够理解为,包名和路径实际上是两个概念,文件名在Golang中不会被显式的引用,一般的引用格式是packageName.FunctionName

结论以下:

  • import导入的是源文件的相对路径,而不是包名。
  • 在习惯上将包名和目录名保证一致,但这并非强制规定(但不建议这么作,这样容易形成调用这个包的人,没法快速知道这个包的名称是什么)
  • 在代码中引用包内的成员时,使用包名而不是目录名。
  • 在一个文件夹内,只能存在一种包名,源文件的名称也没有其余的限制。
  • 若是多个文件夹下有相同名字的package,它们实际上是彼此无关的package。

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

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中的语法区别,也必定会有很多的认知错误。若是在这篇文章中你看到了任何与你的认识有差距的地方,请必定指出做者的错误。若是本文有哪些地方是做者讲的不够明白的,或者是你不理解的,也一样欢迎留言,一块儿交流学习进步。

并且在本文中,不少地方没有进行深刻挖掘,这些做者都有记录,而且打算在以后的文章中,也会从源码的角度出发,分析这些缘由。在这篇文章中,就只是单纯的学会怎么用,就达到目的了。

那么在最后,再次感谢~

相关文章
相关标签/搜索