Golang基本特性及其语法


1. Go语言基础结构

首先我们可以用Java的角度来看待一个class文件是如何构成的

package scau.edu.cn;

这个用来代表这个类所在的包名,那么也就是说我可以用这个包来区分相同的go文件

但是和Java不同的是,我们一般的Main启动类的包名是任意的,但是在Go语言中,我们的启动类是需要定义为

package main

只有这样,这个go文件中的func main才能作为程序启动的go文件

如何执行一个go程序?

package main

import "fmt"

func init() {
	fmt.Println("init输出")
}

func main() {
	fmt.Println("main输出")
}

基于go语言所提供的SDK,输入

go run test.go

通过这个指令,就可以编译运行test.go这个文件

go build test.go

通过这个指令,可以将test.go中的内容进行编译链接,这样的话就可以生成一个可以直接运行的二进制可执行文件,也就是./test.exe

关于package

由于这里是学Java之后转go的,因此对于package要和Java做一个区分

Java中,通常package后面的路径就是文件的类文件路径,也就是说文件路径和package是一一对应的

但是在Golang中,并不要求package后接的路径和文件的路径要完全一致,但是要注意:

同一个文件夹下的文件只能够有一个包名,否则的话编译会报错的

2. Go语言变量

关于命名规则

Go语言的变量名通常是由字母数字下划线组成的,其中第一个字母必须是字母或者是下划线,不能为字母,声明变量的一般形式是var关键字:

var i type// 声明一个变量
var i,j type//声明两个相同类型的变量
package main

import "fmt"

func init() {
	fmt.Println("init输出")
}

func main() {
	// 变量声明示例
	var id string = "123456"
	fmt.Println(id)
	var i, j int = 1, 2
	fmt.Printf("%d %d", i, j)
}

与Java类似的,当我们没有给变量赋予初值的时候,这时候编译器会为我们自动初始化初值,通常来说:

数值类型(包括有complex64/128),数值类型的默认值为0

布尔类型的默认值为false

字符串类型的为””而不是nil

当表示的是引用或者地址的时候,这时候会用nil来初始化这个变量

var integerPoniter *int//声明一个指向整形的指针
var arrayPointer []int//声明一个指向数组的指针
var mapping map[string]int// 声明一个指向map的指针
var integerChannel chan int// 声明一个指向channel的指针,这个channel中存放的数据类型为int
var functionPointer func(string)int// 这表示一个入参为string,返回类型为int的一个函数指针
var errorPointer error// 声明一个类型为error的指针

这里值得注意的一点是,在格式化输出的时候,可以用%v这样的占位符来自动检测你的变量类型

fmt.printf("%v %v %v %v\n",....)

还有一种简洁的写法是由编译器根据表达式的右值来确定表达式的左值的类型

func main() {
	var id = "123456"
	fmt.Println(reflect.TypeOf(id))
}

通过反射工具包,来确定这种确定的方法是合理的

还有一种简介的写法是这样的:

id := "123456"// 这个写法省去了字符串的定义部分,可以说是最常用最简洁的写法

但是要注意这种方式只能够用在函数体的内部,也就是说当声明全局变量的时候必须使用var关键字

// 快速交换两个相同类型变量的值
a,b := b,a

关于变量的生命周期

  • 全局变量生命周期是伴随着程序的运行结束而结束的
  • 局部变量的生命周期是伴随着函数的结束而结束的

3. Go语言的条件选择语句

package main

import (
	"fmt"
)

func init() {
	fmt.Println("init输出")
}

func main() {
	// 1. 声明变量
	localString := "local"
	if localString == "local" {
		fmt.Println("输出local")
	} else if localString == "other" {
		fmt.Println("输出other")
	}
	// 2. 如何通过字典判断是否为空
	var dic map[string]int = map[string]int{
		"apple":  1,
		"orange": 2,
	}
    // 对于map,如何判断为空,可以用这种方法
	if num, ok := dic["apple"]; ok {
		fmt.Printf("apple,%v", num)
	}
	if num, ok := dic["orange"]; ok {
		fmt.Printf("orange,%v", num)
	}
	// switch语句
	switch localString {
	case "local":
		fmt.Println("local")
	case "other":
		fmt.Println("other")
	default:
		fmt.Println("format")
	}
}

4. Go语言的数组与切片

package main

import (
	"fmt"
)

func init() {
	fmt.Println("init输出")
}

func main() {
	// 声明一个数组
	var stringArr = [...]string{"aa", "bb", "cc", "dd", "ee"}
	// 声明一个切片,注意区间,这个区间是[),表示从1截取到3的前一位
	var slice = make([]string, 10)
	slice = stringArr[1:3]
	fmt.Printf("%+v\n", slice)

}

5. Go语言的循环

Go语言中的循环追求一种少就是多的思想,无论是用while型的,for型的,do-while型的,最终都可以使用for这种形式来进行代码的编写

package main

import (
	"fmt"
)

func init() {
	fmt.Println("init输出")
}

func main() {
	// 三种形式写一个从0加到100的循环代码
	sum := 0
	// 1. while
	count := 0
	for count != 101 {
		sum += count
		count++
	}
	fmt.Println(sum)
	// 2. while(true)
	sum = 0
	count = 0
	for {
		sum += count
		if count == 100 {
			break
		}
		count++
	}
	fmt.Println(sum)
	// 3. for
	sum = 0
	for i := 0; i <= 100; i++ {
		sum += i
	}
	fmt.Println(sum)

}

对于集合,如果不希望通过for(;;)这样的形式来进行遍历的话,可以用一个forEach的形式来进行遍历

比如说我们想要执行一个map的深拷贝的话

package main

import (
	"fmt"
)

func init() {
	fmt.Println("init输出")
}

func main() {
	var map1 = map[int]int{}
	for i := 0; i < 10; i++ {
		map1[i] = i
	}
	// 执行map的深拷贝
	var map2 = map[int]int{}
	for key, value := range map1 {
		map2[key] = value
	}
	// 打印map2
	for key, value := range map2 {
		fmt.Printf("%v %v\n", key, value)
	}
}

可以用这样的方式执行一个深拷贝,但是观察一下输出的结果是这样的:

5 5
8 8
0 0
4 4
1 1
2 2
3 3
6 6
7 7

可以看到,它的这个map的底层实现估计也是和JDK中的HashMap的实现类似,也是采用散列表来实现的

下面尝试一下对字符串、数组、切片这几种数据类型进行操作

package main

import (
	"fmt"
)

func init() {
	fmt.Println("init输出")
}

func main() {
	// 声明一个数组
	var stringArr = [...]string{"aa", "bb", "cc"}
	// 1. 遍历这个数组
	for index, value := range stringArr {
		fmt.Printf("%v %v \n", index, value)
	}
	// 2. 定义切片
	var sliceArr = stringArr[1:3]
	for i, s := range sliceArr {
		fmt.Printf("%v %v\n", i, s)
	}
	id := "123456"
	for i, i2 := range id {
		fmt.Printf("%v %c\n", i, i2)
	}
}

关于range的坑

  • for range取不到所有元素的地址
func main() {
	// 1. 声明一个数组
	arr := [2]int{1, 2}
	var res []*int
	// 2. 添加元素
	for _, v := range arr {
		res = append(res, &v)
	}
	fmt.Println(*res[0], *res[1])
}

问题代码以及原因分析:

这里的话我们希望将arr中的元素地址拷贝到res中,然后打印出来这个res的值

我们可以实验出来这样一个结论:当我们使用这个变量v的时候,每次这个变量v的地址都是一样的,所以在这样的情况下,就类似于在循环的时候,就是始终用一个变量来读取数组中的值,这个变量的值和数组中的值是隔离开来的,这样说不太理解,来看一下这样一段代码

int[] arr = new int[5];
int[] res = new int[5];
int v = 0;
for(int i = 0;i < 5;i++){
    v = arr[i];
    res = v;
}

这就好像golang帮我们做了这样一件事,我们每次去取地址的总是同一个变量,这就导致了问题了

如何来解决呢?

  • 第一种方法套娃,就是我们在函数内部,在开辟一个新的变量,用来拷贝这个v就行了
// 2. 添加元素
for _, v := range arr {
	realV := v
	res = append(res, &realV)
}
  • 第二种方法,直接用索引来拷贝
// 1. 声明一个数组
arr := [2]int{1, 2}
var res []*int
// 2. 添加元素
for i := range arr {
	res = append(res, &arr[i])
}
fmt.Println(*res[0], *res[1])

第二个坑,循环是否会停止?

arrSlice := []int{1, 2, 3, 4, 5}
var tempSlice []int
for i := range arrSlice {
	tempSlice = append(tempSlice, arrSlice[i])
}

它的等价代码是这样的:

length := len(arrSlice)
for i := 0; i < length; i++ {
	tempSlice = append(tempSlice, arrSlice[i])
}

6. Go语言的函数

函数定义

func function_name([parameter_list])[return_types]{
    
}

关于函数中的参数传递

golang中的参数传递都是值传递,不会存在一个引用传递

func main() {
	a := 1
	test(&a)
	fmt.Println(a)
}

func test(a *int) {
	*a++
}

这一点可以和C/C++对齐,如果需要用到一个在函数传递的时候改变原参数的值,可以用指针

func main() {
	var arr = []int{1, 2, 3, 5}
	arrTest(arr)
	fmt.Println(arr[0])
}

func arrTest(arr []int) {
	arr[0] = 20
}

字符串类型的可能得根据它的底层原理来看,感觉和Java的那个差不多

func main() {
	var str = "123456"
	test := strTest(str)
	fmt.Println(test)// 输出123456bc
}

func strTest(str string) string {
	str += "bc"
	return str
}

关于函数变量

函数变量这个在C/C++中也有相关的概念,感觉就是一个东西,示例代码:

// 定义内部函数变量
f := func(x int) int {
	return x
}
fmt.Println(f(10))

如何实现Java中类似于函数式编程的效果?

  • 第一步,声明一个函数类型
type fc func(int)int
  • 第二步,定义函数原型
type fc func(x int) int

// 定义回调函数
func callBack(f fc, x int) {
	fmt.Println(f(x))
}

// 定义函数实例
func doSomething(x int) int {
	return x + 10
}

func main() {
	// 注册回调函数
	callBack(doSomething, 10)
}

关于函数闭包

所谓的函数闭包就是一个类似于lambda的匿名表达式,这个表达式直接声明一个匿名函数

// 它想要表达的是:最后会返回一个函数指针,这个函数的入参列表为() 返回参数为int
func getNumber() func() int {
	i := 0
	return func() int {
		i++
		return i
	}
}

func main() {
	nextNumber := getNumber()
	fmt.Println(nextNumber())
	fmt.Println(nextNumber())
	fmt.Println(nextNumber())
	fmt.Println(nextNumber())
	fmt.Println(nextNumber())
	fmt.Println(nextNumber())
	fmt.Println(nextNumber())
}

7. Go语言的常量

  • 普通常量的定义
const identifier [type] = value
  • 常量用作于枚举
const(
    success = 200
    fail = 500
    redis_error = 300
)
  • iota

iota作为一个特殊的常量,可以被认为是一个可以被编译器修改的常量,具体的规则是:

const关键字出现的时候,将被重置为0,const中每新增一行常量声明,都将使得iota++

8. Go语言指针

可以使用new 关键字来创建一个指针,比如说:

s := new(string)
*s = "golang"

9. Go语言的结构体

结构体的定义:

type Student struct {
	id   string
	name string
}

func main() {
	student := Student{
		id:   "",
		name: "",
	}
	fmt.Println(student.id)
}

10. 如何基于Go语言实现面向对象?

在Go语言中,如果想要定义一个类,可以这样做

package domain

type Student struct {
	id    string
	name  string
	phone string
}

// 定义这个类的方法
func (student *Student) getName() string {
	return student.name
}

要注意,当大写开头的时候,那么这时候就是一个public的,当小写开头的时候,那么这时候就是一个private

如何实现继承?

type People struct {
	age int
}

func (people People) GetAge() int {
	return people.age
}

如果要使用父类的属性或者方法的话,那么这时候就需要用这样的方式

type Student struct {
	id     string
	name   string
	phone  string
	people People
}

// GetName :获取当前学生的名称
func (student *Student) GetName() string {
	student.people.GetAge()
	return student.name
}

因此可以看出,go语言中其实是没有继承的,这种面向对象的继承的方式都是通过组合的方式来实现的

Go语言中的接口

如果用Java中的思想来开发接口和实现类的话,那么就可以这样做

package service

type ReaderService interface {
	Read() int
}
package impl

type ReaderServiceImpl struct{}

func (ReaderServiceImpl) Read() int {
	//TODO implement me
	panic("implement me")
	return 0
}

在使用的时候可以这样写:

func doSomething(readerService service.ReaderService) int {
	return readerService.Read()
}

func main() {
	serviceImpl := impl.ReaderServiceImpl{}
	doSomething(serviceImpl)
}

这样的话就实现了面向对象中的多态

11. Go语言中的error体系

由于go语言支持多个返回值,那么这多个返回值中,我们通常会提供一个error作为函数执行的结果评判指标,它其实是一个接口类型,就是一个普通的接口,不会携带任何的堆栈信息,接口的定义如下:

error是什么?

type error interface{
    Error() string
}

通常来说,当发生了一些异常的时候,这时候会通过errors.New()或者是errors.Errorf()来返回一个error的对象,通过这个对象来描述错误信息

一般来说,我们在web开发工程中还会自定义一个struct

type CommonError struct {
	code    int
	message string
}

func (commonError CommonError) Error() string {
	return fmt.Sprintf("code:%v,message:%v", commonError.code, commonError.message)
}

12. 关于defer

defer(延迟),它是go语言中的一个关键字,主要是用在函数或者方法前面,作用是做一个函数和方法的延迟调用

延迟的函数什么时候被调用?

  • 函数执行return的时候
  • 发生panic的时候

延迟调用的语法规则:

  • defer关键字后面表达式必须是函数或者方法调用
  • 延迟内容不能够被括号括起来

defer的执行顺序,也就是说当函数中定义了多个defer的时候,这时候会按照Last In Fast Out的原则进行处理,也就是说是后定义的先被调用(栈的特点:后被压入栈的先被弹出)

defer实战应用

资源的释放

其实可以和Java中的try-catch-finally机制进行类比,这个机制提供了一个入口,通过这个入口就能够定义一个finally的代码

func CopyFile(srcFile string, destFile string) (wr int64, err error) {
	// 1. 获取原文件的内容
	src, err := os.Open(srcFile)
	if err != nil {
		return 0, err
	}
	defer src.Close()
	// 2. 创建新文件
	dst, err := os.Create(destFile)
	if err != nil {
		return 0, err
	}
	defer dst.Close()
	// 3. 调用io库的函数,执行拷贝
	wr, err = io.Copy(dst, src)

	return
}
  • 配合recover一起处理panic

文章作者: 穿山甲
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 穿山甲 !
  目录