Go语法学习(Java转型)

1.环境安装

详情见我另外一篇文章
https://blog.youkuaiyun.com/2201_75669520/article/details/148639277?spm=1001.2014.3001.5502

2.GO语言结构

package main

import (
    "fmt"

    "example.com/GoCode/mathClass"
)

func main() {
    /* 这是我的第一个简单的程序 */
    fmt.Println("nihao!")
    fmt.Println(mathClass.Add(1, 2))
    fmt.Println(mathClass.Sub(1, 2))
}
  1. 第一行代码 package main 定义了包名。你必须在源文件中非注释的第一行指明这个文件属于哪个包,如:package main。package main表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main 的包。
  2. 下一行 import “fmt” 告诉 Go 编译器这个程序需要使用 fmt 包(的函数,或其他元素),fmt 包实现了格式化 IO(输入/输出)的函数。
  3. 下一行 func main() 是程序开始执行的函数。main 函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数(如果有 init() 函数则会先执行该函数)。
  4. 下一行 // 是注释,在程序执行时将被忽略。单行注释是最常见的注释形式,你可以在任何地方使用以 // 开头的单行注释。多行注释也叫块注释,均已以 /* 开头,并以 */ 结尾,且不可以嵌套使用,多行注释一般用于包的文档描述或注释成块的代码片段。
  5. 下一行 fmt.Println(…) 可以将字符串输出到控制台,并在最后自动增加换行字符 \n。
    使用 fmt.Print(“hello, world\n”) 可以得到相同的结果。
    Print 和 Println 这两个函数也支持使用变量,如:fmt.Println(arr)。如果没有特别指定,它们会以默认的打印格式将变量 arr 输出到控制台。
  6. 当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 protected )。

关于包,根据本地测试得出以下几点:

  • 文件名与包名没有直接关系,不一定要将文件名与包名定成同一个。
  • 文件夹名与包名没有直接关系,并非需要一致。
  • 同一个文件夹下的文件只能有一个包名,否则编译报错。

可以看到,我上面的代码中,除了fmt之外,还导了一个我自己的包 “example.com/GoCode/mathClass”

package mathClass

// 添加两个整数
func Add(a, b int) int {
    return a + b
}
package mathClass

// 两个整数相减
func Sub(a, b int) int {
    return a - b
}

也就是这两个函数

一般来说,是创建一个新的文件夹,然后在里面写go程序

每个go程序第一行就需要定义包名,这个包名不一定要和文件夹名一致,但是通常会一致

导包的时候,不能只导mathClass,因为项目使用了Go模块,我们执行过 go mod init 命令,指定了模块路径

例如:

go mod init example.com/hello

因此,现在导包的包名就变成了 模块路径/子包名

“example.com/GoCode/mathClass”

但是我看过拉取下来的代码之后,发现里面的导包都不是这样的,应该是后续框架方面做了更改

3.基础语法

具体看菜鸟教程:https://www.runoob.com/go/go-basic-syntax.html

这里只标记我个人觉得关键的语法区别

3.1赋值

// 全局变量
var (
    a int
    b int
)

func main() {
    a = 10
    b = 5
    // 局部变量
    c := mathClass.Add(a, b)
    d := mathClass.Sub(a, b)
    fmt.Println(c)
    fmt.Println(d)
}

使用 := 赋值操作符,这种赋值方式自动判断类型

但是只能在函数当中使用

函数之外的全局变量,通常通过

var (

a int

b int

)

这种方式声明

注意:

局部变量声明后必须使用

全局变量可以声明后不使用

常量赋值(不可改)

就是const,并且常量赋值后可以不使用

补充:%d和%s

用来拼接整形数字和字符串的

Go 语言中使用 fmt.Sprintffmt.Printf 格式化字符串并赋值给新串:

前者单纯赋值,后者赋值加输出

比如一个字符串长这样

“Code=%d&endDate=%s”

那么它%d和%s那里就可以插入一个整形和字符串

// 直接输出
fmt.Printf("Code=%d&endDate=%s", 123, "2021-12-31")
// 赋值
aaa := fmt.Sprintf("Code=%d&endDate=%s", 123, "2021-12-31")
fmt.Println(aaa)

当然,也有更简单的办法

fmt.Println("Hello","World",123)

3.2Go的运算符(指针)

基础运算符那些其实和Java不能说很像,只能说一模一样

唯一需要在意的就是Go多了个指针

但是不用望而生畏,Go的指针相较于C艹的简单多了,这里简单讲讲区别(因为原来也学过大半年C艹)

从运算上相比:Go的指针仅允许*和&操作,C艹可以做指针运算

从内存上相比:Go也是有GC的,C艹得自己管理

从使用上相比:Go只用来进行值传递和结构体修改,几乎不用多级指针,C艹运用很多,频繁使用多级指针

3.2.1. &

取地址符 : 获取变量的内存地址,返回一个指针类型

c := 20
fmt.Println(&c)

输出:0xc00010c098

3.2.2. *

解引用符 : 通过指针访问或修改其指向的值

c := 20
d := &c
fmt.Println(d)
fmt.Println(*d)

简单来说

指针:就是地址,a的指针,就是保存a的地址

&:获取一个值的指针/地址

*:通过一个指针/地址,获取到这个指针/地址所对应的值

容易混淆的点:

“*”如果放在一个指针前面,那么表示获取这个指针所指向的值

但是“*”如果放在一个type(类型)前面,用于指定变量是作为一个指针

例子如下:

var c *int
c = &a
*c = 10
fmt.Println(*c)

代码中的 c 表示一个int类型的指针变量,用来接收 a 的指针

通过 *c 去修改这个指针指向的值

这就是Go中指针的基本用法

3.2.3. Go指针的常见用法

  • 传递大对象,避免值拷贝
type User struct {
	Name  string
	Age   int
	Email string
}

func UpdateAgeByPointer(u *User, age int) {
	u.Age = age // 直接修改原对象
}

func UpdateAgeByValue(u User, age int) {
	u.Age = age // 只修改副本,原对象不受影响
}

func main() {
	user1 := &User{Name: "Alice", Age: 30}
	UpdateAgeByPointer(user1, 31)
	fmt.Println(user1.Age) // 输出: 31

	user2 := User{Name: "Bob", Age: 25}
	UpdateAgeByValue(user2, 26)
	fmt.Println(user2.Age) // 输出: 25
}

这里的 UpdateAgeByValue 就是类Java方式

但是Java当中,是隐式指针,也就是说,本质上还是 ByPointer 这种方式

Go因为有显式指针,可以我们自己选择是否使用。

运用指针的好处就是,修改的对象是之前的对象

不用指针,本质上是在 UpdateAgeByValue 这个方法里面进行了值拷贝

如果结构体(自建类)很大,那么值拷贝性能就很差了

如果就是想用值拷贝的方式,还想获得修改后的数据,那么就需要从函数里,返回新的对象

func UpdateAgeByValueWithReturn(u User, age int) User {
    u.Age = age // 只修改副本,原对象不受影响
    return u
}
func main() {
	user3 := User{Name: "Charlie", Age: 40}
	user3 = UpdateAgeByValueWithReturn(user3, 41)
	fmt.Println(user3.Age) // 输出: 41
}

通过指针的方式创建结构体实例(常用)

func CreateUser(name string, age int, email string) *User {
    return &User{
        Name: name, 
        Age: age, 
        Email: email,
    }
}

user3 := CreateUser("Charlie", 40, "charlie@example.com")
fmt.Println(*user3) // 输出: {Charlie 40 charlie@example.com}

这样创建实例返回的就是指针,本质上其实和Java当中的构造函数很像,是一种常用的创建实例方式

对比一下

func CreateUser(name string, age int, email string) *User {
    return &User{
        name, 
        age, 
        email,
    }
}

func main() {
    map1 := make(map[int] *User)
    map1[1] = &User{"Alice", 25, "alice@example.com"}
    map1[2] = CreateUser("Bob", 30, "bob@example.com")
}

其实结构体自带了构造方法,但是一般来说,为了可读性和可维护性,专门写一个Create方法是有必要的

一般来说,我们可以把具体的entity类写到专门的包下,和Java一样的规范,然后其他类导包就好了

package main

import (
    "fmt"

    "example.com/GoCode/entity"
)

func main() {
    fmt.Println("Hello, world!")
    user_map := make(map[int]*entity.User)
    user_map[1] = entity.CreateUser(1, "Alice", 20, true)
    user_map[2] = entity.CreateUser(2, "Bob", 25, false)
    fmt.Println(*user_map[1])
    fmt.Println(*user_map[2])
}

总结:

Java里自带隐式指针,之前被娇生惯养习惯了

现在用Go如果想达到之前Java的效果,就必须手动传指针进去

3.3条件和循环语句

3.3.1 条件语句

和Java一模一样

多了个select:类似于 switch 语句,但是select会随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。

3.3.2 循环语句

while和do while直接没了

只有for了(乐)

用法和Java一模一样

关键:

条件和循环语句的用法和Java一模一样,但是区别是不用加(),加了会报错!!!

3.4 函数

Go 语言函数定义格式如下:

func function_name( [parameter list] ) [return_types] {
    函数体
}
  • func:函数由 func 开始声明
  • function_name:函数名称,参数列表和返回值类型构成了函数签名。
  • parameter list:参数列表,参数就像一个占位符,当函数被调用时,你可以将值传递给参数,这个值被称为实际参数。参数列表指定的是参数类型、顺序、及参数个数。参数是可选的,也就是说函数也可以不包含参数。
  • return_types:返回类型,函数返回一列值。return_types 是该列值的数据类型。有些功能不需要返回值,这种情况下 return_types 不是必须的。

没什么特别好说的,上面讲指针的时候就接触过

这里需要介绍几个新的点

3.4.1 函数可以返回多个值

func test(x int , y string) (int, string) {
    return x, y
}


func main() {
    x := 10
    y := "hello"
    c, d := test(x, y)
    fmt.Println(c, d)
}

加个括号就行了,甚至可以返回不同类型

3.4.2 匿名函数

我也称之为,函数内部函数

package main

import "fmt"

func main() {
    // 定义一个匿名函数并将其赋值给变量add
    add := func(a, b int) int {
        return a + b
    }

    // 调用匿名函数
    result := add(3, 5)
    fmt.Println("3 + 5 =", result)

    // 在函数内部使用匿名函数
    multiply := func(x, y int) int {
        return x * y
    }

    product := multiply(4, 6)
    fmt.Println("4 * 6 =", product)

    // 将匿名函数作为参数传递给其他函数
    calculate := func(operation func(int, int) int, x, y int) int {
        return operation(x, y)
    }

    sum := calculate(add, 2, 8)
    fmt.Println("2 + 8 =", sum)

    // 也可以直接在函数调用中定义匿名函数
    difference := calculate(func(a, b int) int {
        return a - b
    }, 10, 4)
    fmt.Println("10 - 4 =", difference)
}

3.4.3 方法

方法是要绑定结构体(Java中的类)的

本质上就是给结构体配置的函数

特点就是可以调用结构体里的成员(Java中类的属性)

package main

import (
   "fmt"  
)

/* 定义结构体 */
type Circle struct {
  radius float64
}

func main() {
  var c1 Circle
  c1.radius = 10.00
  fmt.Println("圆的面积 = ", c1.getArea())
}

//该 method 属于 Circle 类型对象中的方法
func (c Circle) getArea() float64 {
  //c.radius 即为 Circle 类型对象中的属性
  return 3.14 * c.radius * c.radius
}

3.5 数组

3.5.1 基操

和Java有点区别,但不大(感觉每个语言就喜欢整点不一样的,统一下不好吗)

var arrayName [size]dataType

// 声明
var balance [10]float32

// 初始化,像上面那样,不赋值就默认都是0

// 赋值,肯定还是 := 最方便啦
numbers := [5]int{1, 2, 3, 4, 5}

// 不确定长度可以...代替
balance := [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}

// 可以只初始化其中某几个元素
balance := [5]float32{1:2.0, 3:7.0}

// 访问,和java一样
numbers := [5]int{1, 2, 3, 4, 5}
n := numbers[2]
fmt.Println(n)

总的来说,新奇一点的地方就是可以初始化的时候单独赋值其中某几个元素了

3.5.2 多维数组和函数插入

多维没什么好说的,多加一个[]的事情

package main

import "fmt"

func main() {
    // 创建二维数组
    sites := [2][2]string{}

    // 向二维数组添加元素
    sites[0][0] = "Google"
    sites[0][1] = "Runoob"
    sites[1][0] = "Taobao"
    sites[1][1] = "Weibo"

    // 显示结果
    fmt.Println(sites)
}

函数插入数组

func myFunction(param [10]int) {
    ....
}

// 如果想要修改原数组的值,插入指针就好了

func myFunction2(param *[10]int) {
    ....
}

到这里做个小总结

Go的语法和Java的相比,其实就是把很多地方反过来了

例如:

int a = 10;
int b;
b = 10;

int[] arr = new int[10]
a := 10
var b int = 10

arr := [10]int

说实话,Go这种是有点反人类的

3.6 结构体

与类类似,但是很多地方不一样

最大的区别在于

结构体的作用是数据封装,没有方法实现

本质上是创建了一个新的数据类型给我们使用,如果要给当前结构体设置方法,参照 3.4.3

type struct_variable_type struct {
    member definition
    member definition
    ...
    member definition
}

至于结构体的成员访问,调用,结构体传入函数,结构体指针相关的内容

之前在指针那一款就演示过了

直接把它当作自定义的一个新数据类型用就完事了

3.7 切片

简单来说就是没有长度设置的数组,或者说动态数组

3.7.1 定义和初始化

// 直接创建一个没有设置长度的数组
numbers2 := []int {1, 2, 3, 4, 5}
m := numbers2[2]
fmt.Println(m)

// 或者用make更直观一点,len表示初始长度
slice1 := make([]type, len)

// cap表示最大长度,是可选参数
make([]T, length, capacity)

3.7.2 cap和len函数

cap就是获取切片的capacity最大容量

len就是获取切片的现存长度

3.7.3 切片截取

/* 创建切片 */
numbers := []int{0,1,2,3,4,5,6,7,8} 

numbers2 := numbers[1:3]

切片numbers

截取就是numbers[l,r],截取[l,r)的元素,左闭右开。类比Java终端SubString函数

3.7.4 append和copy函数

append就是往切片里面加新的元素,同时也要让当前变量来接收

copy就是把老切片的元素,放到新切片

append的底层逻辑也用到了copy,下面这个是我手写的append方法

内置的append方法len和cap相同时,cap只会+1,我写的是翻倍

func append(s []int, x int) []int {
    if len(s) == cap(s) {
        newSlice := make([]int, len(s), len(s)*2)
        copy(newSlice, s)
        s = newSlice
    }
    s = s[:len(s)+1]
    s[len(s)-1] = x
    return s
}

本质上创建了一个新的切片来装更多的元素,没Java当中的list集合方便

3.8 range

和Java的foreach类似吧

搭配for循环使用的一种遍历工具

基础格式如下

for a,b := range arr {
    fmt.Println(a,b)
    }

其中的 a 和 b 在遍历的时候,面对不同类型的arr,代表的也不一样

a和b的名字可以随意取

面对 数组/切片/字符串时

a是索引下标(一般用i表示),b是下标对应的元素(一般用v表示)

面对map集合时

a是key,b是value

所以说用range遍历map的时候,一般都如下

for key,value := range map{
    fmt.Println("key = ", key, " value = ", value)
    }

还有一个就是忽略索引和忽略值

遍历的时候是可以忽略索引/忽略值的

package main

import "fmt"

func main() {
    nums := []int{2, 3, 4}
    
    // 忽略索引
    for _, num := range nums {
        fmt.Println("value:", num)
    }
    
    // 忽略值
    for i := range nums {
        fmt.Println("index:", i)
    }
}

3.9 Map集合

func main() {

    user_map := make(map[int]*entity.User)
    user_map[1] = entity.CreateUser(1, "Alice", 20, true)
    user_map[2] = entity.CreateUser(2, "Bob", 25, false)
    fmt.Println(*user_map[1])
    fmt.Println(*user_map[2])

    v1 := user_map[1]
    v2 := user_map[2]
    v1.Name = "Alice1"
    v2.Name = "Bob1"
    fmt.Println(*v1)
    fmt.Println(*v2)

    delete(user_map, 1)

    for key, value := range user_map {
        fmt.Println(key, *value)
    }
}
user_map := make(map[int]*entity.User)

[]中的是key的类型,后面是value的类型

调用那些和数组差不多

delete的用法就是 集合名+key

3.10 接口

接口本身的用处和定义就不过多阐述了,都是Java转的了,接口用的还少吗(乐)

3.10.1 基本结构

/* 定义接口 */
type interface_name interface {
    method_name1 [return_type]
    method_name2 [return_type]
    method_name3 [return_type]
    ...
    method_namen [return_type]
}

/* 定义结构体 */
type struct_name struct {
    /* variables */
}

/* 实现接口方法 */
func (struct_name_variable struct_name) method_name1() [return_type] {
    /* 方法实现 */
}
...
func (struct_name_variable struct_name) method_namen() [return_type] {
    /* 方法实现*/
}
type Shape interface {
	Area() float64
	Perimeter() float64
}

type Circle struct {
	radius float64
}

func (c Circle) Area() float64 {
	return 3.14 * c.radius * c.radius
}

func (c Circle) Perimeter() float64 {
	return 2 * 3.14 * c.radius
}


func main() {
	c := Circle{radius: 5}
	fmt.Println(c.Area())
	fmt.Println(c.Perimeter())
}

3.10.2 空接口

所有类型的超集,所有类型都实现了空接口

主要用于需要传入或存储任意类型的场景:泛型容器、通用参数

空接口:interface{}

要知道空接口的实例用法,看下一章的类型转换

3.11 类型转换

3.11.1 基本格式

除了整形转浮点型,可以直接类似Java那样转之外,其他类型转换都需要用到一个包:strconv

需要调用这个包当中的方法,进行类型转换

下面列出转换的格式

import (
    "fmt"
    "strconv"
)

func main() {
    num := 123
    str := strconv.Itoa(num)
    fmt.Printf("整数 %d  转换为字符串为:'%s'\n", num, str)

	num2, err := strconv.Atoi(str)
	if err!= nil {
		fmt.Println("字符串转换为整数失败:", err)
		} else {
		fmt.Printf("字符串 '%s' 转换为整数为:%d\n", str, num2)
		}
}

这里就可以看到两种不同形式的语法

因为大部分类型转换的时候,都有可能发生错误,因此strconv中的方法在转型时,都会返回两个值:转换后的值,错误

第一种:整形转字符串

不可能发生错误,因此只有一个返回值

第二种:字符串转整形

可能发生错误,因此标准格式是,接收后判断是否有错,然后再使用转型后的值

还有其他很多类型转换的方法,可以写业务时问AI即可

3.11.2 接口类型转换

接口的类型转换分为两种形式:类型断言类型转换

类型断言

还是先看格式

value.(type) 
或者 
value.(T)

value 是接口类型的变量,type 或 T 是要转换成的类型。

实例

package main

import "fmt"

func main() {
    var i interface{} = "Hello, World"
    str, ok := i.(string)
    if ok {
        fmt.Printf("'%s' is a string\n", str)
    } else {
        fmt.Println("conversion failed")
    }
}

简单来讲,就是和strconv的类型转换一样,转换的时候,会给转换后的值一个判断是否成功的条件

因此格式也和strconv的一样

先判断是否转换成功

再使用转换后的值

类型转换(接口)

格式

T(value)

这个其实就是Java当中常见的类型转换了

T 是目标接口类型,value 是要转换的值。

这种方式用的很少,也就是我之前说的,整形转字符串,byte转字符串这种可以用

实际运用

接口类型转换的实际运用其实要集合两个东西:多态和空接口

多态运用

package main

import "fmt"

type Shape interface {
    Area() float64
}

type Rectangle struct {
    width, height float64
}

type Circle struct {
    radius float64
}

func (r Rectangle) Area() float64 {
    return r.width * r.height
}

func (c Circle) Area() float64 {
    return 3.14 * c.radius * c.radius
}

func printShapeInfo(s Shape) {
    fmt.Printf("面积: %.2f\n", s.Area())

    // 使用类型转换区分不同的具体类型
    switch v := s.(type) {
    case Circle:
        fmt.Printf("这是一个圆,半径: %.2f\n", v.radius)
    case Rectangle:
        fmt.Printf("这是一个矩形,宽: %.2f, 高: %.2f\n", v.width, v.height)
    default:
        fmt.Println("未知形状")
    }
}

func main() {
    var s1 Shape = Circle{10}
    var s2 Shape = Rectangle{10, 20}
    printShapeInfo(s1)
    printShapeInfo(s2)
}

可以去判断当前接口被实现的类型,然后做区分

空接口运用

package main

import "fmt"

func processValue(v interface{}) {
    // 判断 v 是否为字符串类型
    if str, ok := v.(string); ok {
        fmt.Printf("接收到字符串: %s\n", str)
    }

    // 判断 v 是否为整数类型
    if num, ok := v.(int); ok {
        fmt.Printf("接收到整数: %d\n", num)
    }
}

func main() {
    processValue("hello") // 输出: 接收到字符串: hello
    processValue(42)      // 输出: 接收到整数: 42
    processValue(3.14)    // 无输出,因为未匹配任何类型
}

因为空接口是个超集,所以类都是它的实现类,因此可以传进去单独判断

本质上也是多态运用

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值