golang中的程序结构

写在前面

今天来聊一聊golang中程序结构相关的话题,具体包括条件语句、循环、函数和指针等内容。

条件语句

if语句

给定一个自然数v,如果它在0-100之间则返回v,若大于100则返回100,小于0则返回0,使用Go语言实现的代码如下:

package main
import "fmt"
func ifTest(v int) int{
if v >100 { //if的条件里面不需要括号
return 100
}else if v <0 {
return 0
}else {
return v
}
}
func main() {
var a int = ifTest(5)
fmt.Println(a)
}
//运行结果:
5

注意到没有if的条件里面不需要括号,创建的文件中不能包含下划线。func ifTest(v int) int中参数v的前面不需要添加var关键词,后面的int则是该函数的返回结果。

现在来使用Go语言来读取某个文件的信息,如test.txt中的内容,相应的代码如下:

package main
import (
"fmt"
"io/ioutil"
)
func readFieTest(){
const filename = "test.txt"
contents, errorinfo :=ioutil.ReadFile(filename)
//var contents ,errorinfo = ioutil.ReadFile(filename)
if errorinfo != nil {
fmt.Println(errorinfo)
}else{
fmt.Printf("%s\\n",contents)
}
}
func main() {
readFieTest()
}

读取文件需要采用ioutil包中的ReadFile函数,查看源码可知该函数一次可以返回两个值:

func ReadFile(filename string) ([]byte, error) {
......
}

if errorinfo != nil中的nil就是无的意思,此处就是产生了错误,可以参考这篇文章了解更多关于nil的信息:理解Go语言的nil 。其实上面那种方式不是很简介,可以使用类似于Java中的三元表达式:

package main
import (
"fmt"
"io/ioutil"
)
func readFieTest(){
const filename = "test.txt"


if contents, errorinfo := ioutil.ReadFile(filename) ;errorinfo != nil {
//先运行前半句后进行判断
fmt.Println(errorinfo)
}else {
fmt.Printf("%s",contents)
}
}
func main() {
readFieTest()
}

发现没有if的条件里可以进行赋值,且if条件里赋值的变量作用域就是这个if语句。接下来聊一聊switch,很多语言中都有switch。

switch语句

switch后面是可以接表达式的(也可以不接),使用Go实现计算某两个整数的加减乘除的功能,相应的代码如下:

package main
import "fmt"
func eval(a, b int,operation string) int {
var result int
switch operation {
case "+":
result = a +b
case "-":
result = a-b
case "*":
result = a*b
case "/":
result = a/b
default:
panic("不支持的运算方式"+operation) //这个panic就是报错,让程序停下来
}
return result
}
func main() {


test:= eval(3,4,"*")
fmt.Println(test)
}
//运行结果:
12

细心的你发现什么奇特之处么?对,里面没有break,因为Go语言中的switch会自动break,除非使用了fallthrough。而在C、C++或者是Java中你要么在后面添加break要么添加continue。

再来举一个例子,用于判断学生成绩情况:当分数小于60,则显示不及格;60-69为及格;70-79为中等;80-89为良好;90-100为优秀。使用Go语言实现的代码如下:

package main
import "fmt"
func scoreTest(score int)string {
var result string = ""
switch {
case score <0 ||score >100 :
panic(fmt.Sprintf("无效的分数:%d",score)) //如果这个条件成立,则程序不再往下执行
case score <60:
result = "不及格"
case score<70:
result = "及格"
case score <80:
result = "中等"
case score <90:
result = "良好"
case score <=100:
result = "优秀"
}
return result
}
func main() {
fmt.Println(
scoreTest(59),
scoreTest(62),
scoreTest(77),


scoreTest(84),
scoreTest(99),
//scoreTest(-99),
)
}
//运行结果:
不及格 及格 中等 良好 优秀

如果程序满足panic的要求,则程序会停止运行。switch后面可以没有表达式

循环语句

for语句

使用Go语言实现求解0-指定数字内的数字之和,如100以内整数的和,相应的代码如下:

package main
import "fmt"
func sumTest(a int) int {
sum := 0
for i :=0;i<=a;i++ {
sum+=i
}
return sum
}
func main() {
fmt.Println(sumTest(100))
}

上面使用了for循环,可以发现这个for循环的格式除了条件中不包含括号以外,其实和Java,JavaScript的代码完全一致。且大家要学会在函数中尽量使用:=的方式替代var来声明变量。

for的条件中不包含括号,且条件中可省略初始条件,结束条件以及递增表达式

再来看一个例子,将整数转换成二进制的表达式,相应的代码如下:

package main
import (
"fmt"
"strconv"
)
func intToBinary(n int)string {
result := ""
for ;n>0;n/=2{ //省略初始条件,相当于while
lsb := n%2
result = strconv.Itoa(lsb) +result
}
return result
}
func main() {
fmt.Println(
intToBinary(5), // 101
intToBinary(13), //1101
intToBinary(121242),
)
}

再来换一种方式读取之前那个test.txt文件中的内容,现在是一行行的进行读取:

//一行行读取
func printFileTest(filename string){
file, err :=os.Open(filename)
if err != nil{
panic(err) //程序停下来去报错
}else{
scanner := bufio.NewScanner(file)
for scanner.Scan(){
// 这里既没有开始条件,也没有递增条件,只有结束条件,此时分号都可以不写,Go语言中没有while


fmt.Println(scanner.Text()) //输出
}
}
}
func main() {
printFileTest("test.txt")
}

在这段代码里面for中既没有开始条件,也没有递增条件,只有结束条件,那么此时的分号都可以不写,记住Go语言中没有while。因为while的功能和for相似,所以Go语言中就没有必要存在while这个关键词了。

当for中什么也不加,则变成了一个死循环,就相当于其他语言中的while true。Go语言中的死循环实现起来非常简单,那是因为后面会经常使用到死循环。

简单总结一下循环语句的特点:1、for和if条件后面没有括号;2、if条件里面也可以定义变量;3、Go语言中没有while;4、switch中不需要定义break,也可以直接switch多个语句。

函数

其实在前面我们就使用了func这个关键词用于定义函数,函数定义的格式为:

func 函数名称(参数名称,参数类型)返回值类型{
......
}


需要说明的是,Go语言的函数可以有多个返回值的,且类型可以不相同:

package main
import "fmt"
//求解两个数的和
func sumTest(a,b int)int{
return a+b
}
//求两个数相除的商及余数
func divTest(a ,b int) (int,int, string) {
return a/b, a%b, "你好"
}
func main() {
fmt.Println(sumTest(3,6))
fmt.Println(divTest(13, 4))
}
//运行结果:
9
3 1 你好

在上面的代码中不知道返回的到底是什么,只知道都是int类型,其实可以像声明变量的方式那样给返回值设置名称:

//求两个数相除的商及余数
func divTest(a ,b int) (q, r int, s string) {
return a/b, a%b, "你好"
}

由于Go语言非常严格,定义的变量一定要使用,如果函数有多个返回值,我们只想取某个值时,那么其余的变量可以使用匿名变量_来接收。尽管Go语言支持返回多个类型值,但是不要乱用,一般返回两个,前者是数据,后者是错误nil,如下图所示。将前面实现两个数的四则运算的相关代码进行改写:

package main
import "fmt"
func calcTest(a, b int,operation string ) (int, error) {
switch operation {
case "+":
return a+b, nil
case "-":
return a-b,nil
case "*":
return a*b,nil
case "/":
return a/b,nil
default:
return 0,fmt.Errorf("不支持的运算操作:%s",operation)
}
}
func main() {
fmt.Println(calcTest(3,5,"+"))
}
//运行结果:
8

上述代码其实还不够完善,在main方法中对正常与否需要进行判断:

func main() {
if result ,err:= calcTest(3,5,"+");err != nil{
//程序运行存在错误
fmt.Println("程序运行存在错误",err)
}else{
fmt.Println(result)
}
}

在Go语言中函数可以返回多个值,且可以给多个值声明名称,但是返回多个值的情况仅仅适用于非常简单的函数,不过取不取名字和调用者无关。

Go语言是函数式编程,函数是一等公民(Python中也是),函数里面的参数,返回值里面都可以包含函数。通过前面求两个数的四则运算这个例子进行改写,实现将函数作为参数:

Go语言没有其他语言中的默认参数、可变参数、函数重载等,只有一个可变参数列表:

//求可变参数列表中参数之和
func dynamicVariable(values ... int)int {
sum :=0
for i:=range values{
sum+=values[i]
}
return sum
}
func main() {
fmt.Println(dynamicVariable(1,2,3,4,5,6))
}
//运行结果:
21

函数小结:1、函数返回值的类型写在最后面;2、函数可以返回多个值;3、函数可作为参数进行使用;4、没有默认参数和可选参数,函数重载等。

指针

大家不要听到指针就害怕,Go语言中的指针和C语言中的指针差别很大(Go语言中的指针不能运算,而C语言中却可以),比C中的指针简单多了。

看到这里就必须谈到一个老生常谈的问题:Go语言中的参数传递是值传递还是引用传递?在C和C++中既可以值传递也可以引用传递。Java和Python绝大部分都是引用传递,除了系统的自建类型以外。那么什么是值传递?什么是引用传递呢?我们通过C++中的一段代码进行了解(C++中使用&表示引用传递):

void pass_by_value(int a){ //值传递
a++;
}
void pass_by_guide(int& a){ //引用传递
a++;
}
int main(){
int a =3;
pass_by_value(a)
printf("值传递以后的值为:%d\\n",a);
pass_by_guide(a)
printf("引用传递以后的值为:%d\\n",a);
}
//运行结果:
3 4

pass_by_value是值传递,会将a的值从main函数中拷贝一份到pass_by_value函数中,真正作了一份拷贝,拷贝进去的a加了1,那么main函数中的a并没有发生变化,没有动依旧是3。pass_by_guide是引用传递,它不会拷贝,此时main函数中的a和pass_by_guide中的a其实是引用了同一个变量a,因此在pass_by_guide函数中进行了加1操作,自然main函数中的a也会发生变化,因此就变成了4。值传递就是拷贝,原来值不会发生变化;引用传递不会拷贝,会导致原来的值发生变化。

Go语言只有值传递一种方式。Go语言中的参数需要配合其指针来使用,具体分情况:



上面这种就是值传递,两者没有影响。下面是使用到了指针的情况:



左侧是一个int类型名为a的变量,右侧是一个int类型名为aa的指针,通过指针实现相当于引用传递的效果,把a的地址给了你以后,可以修改a的值。这些都是基本数据类型,再来尝试一个自定义类型:



当把左侧的book对象传给右侧的read函数时,一般这个book对象本身通常包含指向data的一个指针,然后拷贝一份到右侧函数中,右侧的book对象也有一个pdata,但是是指向同一个data,其实就是拷贝了同一份指针。因此在Go语言中,自定义类型的时候需要考虑把它作为一个值还是一个指针来用。这里的book其实就是作为一个值来用。

用一个交换两个对象的值这个例子来加深大家的印象:

//交换两个对象的值
func swap(a,b int) {
a,b = b ,a
}
func main() {
a ,b := 1,2
swap(a,b)
fmt.Println(a, b)
}
//运行结果:
1,2

你会发现这个函数没有用,两个数值并没有发生交换,的确是这样的,那是因为这个需要借助于指针来完成:

//交换两个对象的值
func swap(a,b *int) {
*a,*b = *b ,*a //声明指针需要使用*


}
func main() {
a ,b := 1,2
swap(&a,&b) //传递地址需要使用&
fmt.Println(a, b)
}
//运行结果:
2,1

不过这种看起来挺麻烦的,其实之前的代码不是没有起作用,而是没有将结果进行返回,修改一下代码其实是可以的:

func swapTest(a,b int)(int ,int) {
return b ,a
}
func main() {
a ,b := 1,2
a, b = swapTest(a,b)
fmt.Println(a, b)
}
//运行结果:
2,1

这样就通过接收函数的返回值,进而实现交换两个数值的目的。至此程序结构部分就聊到这里,后续介绍内建容器和面向对象,面向接口等方面的知识。