Go语言学习分享(简单与Java作比较)

目录

 

序言

Java和Go语言区别

数据类型

变量、方法的访问控制

代码块执行顺序(不完整)

循环结构

条件语句

函数

数组

指针

结构体

切片

Map集合

递归函数

接口

异常处理

并发


序言

       之前有过一年多的Java开发经验,主要学习了Java基础(包含面向对象语言特点——封装继承多态、异常处理、常用类、数组和集合、IO流),JVM内存机制,设计模式,数据库设计,以及在工作中用到的一些开发框架,了解并熟悉别人的框架有助于简化自己的开发。本篇文件较为啰嗦,且无侧重点,慎入。

       比如Java应用中常用的spring/springboot/springcloud框架,主要特点就是IOC和AOP。我们工作中使用到过spring和springboot框架(springboot是spring框架的升级,简化了大量的配置信息,如application.properties+dubbo.xml+mybatis-config.xml+...在springboot里直接配置在application.yml文件里),其中的控制反转(Inversion of Control)也可以叫依赖注入,Controller层通过@Controller或@RestController(相当于@ResponseBody + @Controller,http请求需要返回JSON,XML或自定义mediaType内容则加上@ResponseBody 注解)进行组件注入,Service层通过@Service(注意是实现类而不是服务接口)注解进行组件注入,Dao层通过@Repository进行组件注入,Model层和Api层没有需要进行依赖注入的类,不过有些特殊的组件也可以通过其他注解进行依赖注入,如配置类@Configuration、@Bean、@Autowired、@Resource等,可以通过注解的方式注入到应用上下文中(常用的依赖注入除了注解注入还有接口注入、构造方法注入、setter方法注入),默认情况下注入的组件都是单例,如果需要产生的bean是多例可以在配置中修改scope属性="prototype"。而面向切面变成(Aspect Oriented Programming)是指通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。它是从程序运行角度考虑程序的结构,从处理业务过程的切面提取出重复的业务过程,最常用的如记录日志、监控性能、权限控制、缓存优化、事务管理。使用@Aspect注解+@Before/@After/@AfterThrowing/@Around对某个方法进行前置增强/后置增强/异常增强/环绕增强。

      上面说到的只是java web开发的主流框架,但光有java web的空壳子没有用,还需要有底层数据。这也就需要用到数据库连接(Java Database Connectivity,JDBC)的持久化框架,常用的ORM(Object Relational Mapping)框架有Mybatis/Mybatis-plus、Jpa、Jdo和Hibernate,对象关系映射框架是用来将程序中的实体类和数据库字段进行一一映射,然后使用SqlSessionFactory工厂创建的SqlSession对象来操作数据库的。当业务多了后,所有的东西不可能写到一个服务中去,数据也不可能就靠简单的服务器集群就可以搞定,更多的情况是进行服务拆分,这时就可能用到分布式服务框架dubbo(当然不止阿里的dubbo,还有High Speed Framework,腾讯的Total Application Framework,京东的Java Service Framework,新浪的Motan),使用分布式服务虽然能够将业务进行横向或者纵向的拆分,但也可能在使用的过程中出现一些问题,比如在RPC调用时请求某个接口的超时时间设置不当可能会造成服务不稳定。还有为了减轻数据库压力,我们肯定也会用到分布式缓存,常见的就是redis,一些调用参数不变但请求次数很多的接口通常会加上缓存,缓存还可以存放一些有时限的数据,如登陆验证码、链接等。有些服务为了按时按点执行,我们还需要用到定时任务服务,如ElasticJob、TBSchedule等。再或者为了解耦、异步、消峰我们还会用到像kafka这样的消息中间件,当然这是针对实时性要求不高的服务。

      不管是学习C语言、C++、Java还是Go、Python,前面学到的东西并不会白费,有些时候反而能互补,比如有些设计思想。

      搭建环境那都是一样的就不说了。下面就主要以Java和Go语言作对比,Java是面向对象的语言,Go语言既不是面向对象的语言也不是面向过程的语言,但Go语言在其他语言的身上能找到自己的影子。

      Java所有开发都是以对象来的,虽然Java的基本数据类型char,boolean,byte,short,int,long,float,double,但它们都有对象包装类——Character,Boolean,Byte,Short,Integer,Long,Float,Double,实例化一个对象只需要new出来,它就能创建在JVM的堆里,至于使用完了垃圾回收大家可以自己去查阅资料。这里需要注意的一个地方就是,基本类型int转换为包装类型Integer时,在[-128,127]内转换的数据相等,在其他范围相同的int数据转换的包装类值不相等。这是由于Integer类为内部整数维护一个内部静态类IntegerCache缓存并保存从-128到127的整数对象,所以在这区间转换出来的是相等的对象,如下图。

    public static void main(String[] args) {
        System.out.println("实例化相同值对象结果="+(new Integer(127) == new Integer(127)));
        System.out.println("实例化相同值对象结果="+(new Integer(128) == new Integer(128)));
        System.out.println("将基本类型int转换为包装类型Integer结果="+(Integer.valueOf(127) == Integer.valueOf(127)));
        System.out.println("将基本类型int转换为包装类型Integer结果="+(Integer.valueOf(128) == Integer.valueOf(128)));
        Integer a1 = 127;
        Integer a2 = 127;
        Integer b1 = 128;
        Integer b2 = 128;
        System.out.println(a1 == a2);
        System.out.println(b1 == b2);
    }

Java和Go语言区别

数据类型

  • 上面其实也提到过Java的基本数据类型,其实Java除了八大基本数据类型(char,boolean,byte,short,int,long,float,double)外还有引用数据类型,如对象(包含string)、数组。
  • 而Go语言数据类型包含布尔型(bool),数字型(uint8、uint16、uint32、uint64、int8、int16、int32、int64、float32、float64、complex64、complex128),字符串型,派生类型(指针Pointer、数组、结构化类型、Channel、函数、切片、接口、Map),布尔型、数字型、字符串型的默认值和Java差不多,不过Java的引用数据类型Java是null,Go的派生类型是nil。
  • Java中占用字节大小:char-2字节,boolean-1字节(boolean类型数组的访问与修改共用byte类型数组的baload和bastore指令时,boolean数组是1个字节;boolean值在编译后使用Java虚拟机中的int数据类型来代替时boolean值是4个字节,根据JVM是否按照规范来判断),byte-1字节,short-2字节,int-4字节,long-8字节,float-4字节,double-8字节。Go中的数据类型占几个字节特别好算吧,直接除以8bit,比如uint16即16/8=2字节。

变量、方法的访问控制

  • Java会有访问控制符和非访问控制符,访问控制符可以使类、变量、方法和构造方法具有不同的访问权限,public > protected > default > private(当前类、同包子类、不同包子类、不同包其他类的访问)。而非访问控制符有static修饰符(可修饰类、变量、方法,用来声明独立于对象的静态变量),final修饰符(类——不能被继承、方法——不能被继承类重定义、变量——不可修改),abstract修饰符(创建抽象类和抽象方法,不能与final一起用),synchronized修饰符(保证被修饰的方法线程安全),transient修饰符(实现Serialize接口的类中被修饰的变量JVM不进行序列化,但静态变量不管有没有修饰符都不能被序列化,且实现Externalizable接口的类与transient无关),volatile修饰符(被修饰的变量线程共享,保证线程可见性和禁止指令重排序)
  • 而Go中不同包下的不同变量和方法只需要通过大小写就可以进行访问控制,大写字母开头的变量和方法是公开的,而小写的是私有的。

代码块执行顺序(不完整)

  • Java代码块的执行顺序是:父类非静态代码块 ——> 父类构造函数 ——> 子类非静态代码块 ——> 子类构造函数
  • Go代码块的执行顺序是:其他包go文件定义变量初始化 ——> 其他包go文件定义的init()函数执行 ——> 本包go文件定义变量初始化 ——> 本包go文件定义的init()函数执行 ——> 本包go文件定义的main()函数执行

循环结构

  • Java循环结构有while循环,do...while循环,for循环,用到的控制语句有break、continue。
  • Go语言中循环结构主要就是for循环以及循环嵌套,里面会用到的控制语句有break、continue、goto,多了一条goto语句,它可以无条件地转移到过程中指定的行,但是在结构化程序设计中一般不主张使用 goto 语句, 以免造成程序流程的混乱,使理解和调试程序都产生困难。
  • Go语言中的for循环除了for (init;condition;increment){conditional code}外,还可以使用for (key,value := range oldMap){newMap[key] = value}对slice、map、数组、字符串等进行遍历。Go语言里()可以省略,有兴趣的同学还可以拿Go的for range遍历和Java的Iterator迭代器进行比较。

条件语句

  • Java的条件语句主要有if语句,if...else语句以及if嵌套语句。
  • Go的条件语句有if语句,if...else语句,if嵌套语句,switch语句(switch 变量+case分支+default分支,默认匹配后不会执行其他case,使用 fallthrough 会强制执行后面的 case 语句,fallthrough 不会判断下一条 case 的表达式结果是否为 true),select语句(Go的一个控制结构,类似switch,但它会随机执行一个可运行的 case。如果没有 case 可运行,它将阻塞,直到有 case 可运行。一个默认的子句应该总是可运行的。)

函数

  • Java虽然没有函数的概念,但是通常可以用类中的方法去进行实现各个功能模块。
  • Go语言中至少有一个main()函数,也可以加一个init()初始化函数。
  • Go语言中函数参数可以用是值传递也可以是引用传递,值传递在函数内对参数进行修改不会影响到实际参数,引用传递在函数内对参数进行修改会影响到实际参数。这个和Java方法参数的值传递(基本数据类型)和引用传递(引用数据类型)一样。
  • Go语言函数一个函数可以作为另一个函数的实参,如下图左边是工具包里的go文件,声明了一个函数类型func(int) int,符合该函数类型的所有函数都可以作为TestCallBack的参数,右边是函数调用。

package util

import "fmt"

//声明一个函数类型
type callBackType func(int) int

func CallBack(x int) int{
	fmt.Printf("我是回调函数,x:%d\n",x)
	return x
}

func TestCallBack(x int, function callBackType){
	function(x)
}
package main

import(
	"fmt"
	"godemo/src/gocode/callbackDemo/util"
)

func main(){
	//CallBack函数作为实参传入到TestCallBack函数
	util.TestCallBack(1, util.CallBack)
	//匿名函数作为实参传入到TestCallBack函数中,类似于Java的匿名内部类
	util.TestCallBack(2, func(x int) int {
        fmt.Printf("我是回调,x:%d\n", x)
        return x
    })
}
  • Java中虽然一个方法不能作为另一个方法的实参,但是Java 8引入的新特性函数式接口可以将不同的条件的方法作为参数。Predicate接口通过@FunctionalInterface实现函数式接口,它可以定义一组条件并确定指定对象是否符合这些条件。下面的eval方法中的第二参数可以传入不同的条件。

public static void main(String args[]){
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
        /**
         *  Predicate<Integer> predicate = n -> true
         *  n 是一个参数传递到 Predicate 接口的 test 方法
         *  n 如果存在则 test 方法返回 true
         */
        System.out.print("输出所有数:");
        eval(list, n->true);
        /**
         * Predicate<Integer> predicate1 = n -> n%2 == 0
         * n 是一个参数传递到 Predicate 接口的 test 方法
         * 如果 n%2 为 0 test 方法返回 true
         */
        System.out.print("输出所有偶数:");
        eval(list, n-> n%2 == 0 );

        // Predicate<Integer> predicate2 = n -> n > 3
        // n 是一个参数传递到 Predicate 接口的 test 方法
        // 如果 n 大于 3 test 方法返回 true

        System.out.print("输出大于 3 的所有数字:");
        eval(list, n-> n > 3 );
    }

    public static void eval(List<Integer> list, Predicate<Integer> predicate) {
        for(Integer n: list) {
            if(predicate.test(n)) {
                System.out.println(n + " ");
            }
        }
        System.out.println();
    }
}

数组

  • Java和Go的数组基本上是没有区别的,声明和初始化数组的方式基本类似,也都是通过索引访问数组的元素,这里就不多说了。

指针

  • Java中是没有指针概念的,Java语言想让程序员更关注于业务,一个对象不实例化它就是null不会占用内存空间,也就不会有内存地址。实例化的对象它们的地址也有可能发生变化,比如GC复制算法在进行对象的垃圾回收时,有些还在使用的对象的地址可能会发生变化。Java中的unsafe类可以获取相对初始地址的偏移量来查看当前对象目前的地址,一般不需要关注。
  • Go语言中和C/C++一样也用到了指针,使用指针可以更简单的执行任务,指针也带来了更高的效率。指针被定义后没有分配到任何变量时它就是nil指针。
  • Java中对某个对象/数组进行修改时,需要通过方法的返回值的对象/数组来接收新的对象/数组,而Go语言有指针后可以向函数传入指针,直接通过指针修改对应的值。

结构体

  • Go中结构体用来定义不同的数据类型,语法type struct_name struct{member definition ... member definition ... }

切片

  • Go语言切片是对数组的抽象。数组长度不可变,但切片长度和容量是可变的。
  • 切片包含三个属性:指针(Pointer)、长度(Length)、容量(Capacity),可以使用内建函数make([]type, length, capacity)创建特定类型的切片,也可以从其他切片中进行截取。
  • 这里需要区分一下数组和切片,数组是长度不可变的,而切片是长度可变的。数组的类型是[length] type,切片的类型是[] type。
  • 创建一个切片事实上是指向一个底层数组,如果对切片进行截取后,修改截取后的切片的元素值实际上改变的是底层数组的元素值,截取前的切片元素值也会发生改变。
  • 切片可以通过内建函数copy()和append()进行切片拷贝以及切片元素追加。
  • 切片元素追加时需要注意,如果原切片的容量大于长度,则直接在原切片的基础上追加,切片容量不变且长度+1;如果原切片的容量等于长度,则会对原切片进行2倍扩容且长度+1,这时将会生成一个新的底层数组,即原切片和扩容后的切片不再是共用一个底层数组,修改扩容后的切片的元素值将不会影响原切片的元素值。示例如下,

	old_slice := []int{11,22,33,44,55,66,77,88}
	test_slice := old_slice[1:3:3]	//从1开始切,长度3-1,容量3-1,即{22,33}
	fmt.Printf("追加前的slice=%v,长度=%v,容量=%v\n",test_slice,len(test_slice),cap(test_slice))
	app_slice := append(test_slice, 100)
	fmt.Printf("追加后的slice=%v,长度=%v,容量=%v\n",app_slice,len(app_slice),cap(app_slice))
	app_slice[0] = 300
	fmt.Printf("修改扩容后的切片的第一个元素后,追加前slice=%v, 追加后slice=%v\n",test_slice, app_slice)
  • 还需注意的是底层数组扩容时,长度不超过1024则按照当前底层数组的长度的2倍进行扩容,并生成新数组;长度超过1024时,按照25%的比率扩容,也就是1024个元素存在时,容量扩展为1280(本机的go版本是1.15.7)

Map集合

  • 它是一种无序的键值对集合。但是其底层实现是一个散列表,散列表中有两个结构体hmap(a header for a go map)+ bmap(a bucket for a go map,通常也叫bucket)。Go中它是数组+链表,使用链表的原因和Java一样是为了避免哈希冲突。而Java在Jdk1.8后是数组+链表+红黑树,在桶元素个数大于64或小于6时会从链表和红黑树结构相互转换,引入红黑树主要是为了提高查询效率。
  • Go map进行扩容时也是根据加载因子(loadFactor)进行判断的,Go语言中的加载因子=map长度/2^B(B代表bmap数组的长度,B是通过key进行hash运算的低位的位数),而Java语言中的加载因子默认是0.75f。

递归函数

  • 递归,就是在运行的过程中调用自己。
  • 不管是Go、Java、还是其他语言都可以实现递归调用,最常见的递归案例就是实现斐波那契数列。

接口

  • Go的接口是对其他类型行为的抽象和概括;它通过type interface_name interface定义。而Java中接口要优先于实现类,通过implements 关键字绑定接口和实现类的关系。
  • Go是通过func(struct_name_variable  struct_name) method_name()[return_type]{ //方法实现}来重写方法的。Java是通过接口实现类去实现接口并重写(Override)方法的。

异常处理

  • 我们知道Java异常分为Error(程序无法处理的严重错误,编译器不做检查,通常JVM会终止线程的动作)和Exception(运行时异常和编译期异常以及自定义异常),而Exception异常通常可以通过try{}catch(Exception){}语句进行捕获处理。
  • 而Go语言中进行异常处理时,可以通过defer+recover()+panic()来处理异常。defer是延迟语句,当主函数退出后才进行调用,遵循FIFO原则;recover()和panic()都是builtin包下的内建函数。在defer的函数中,执行recover()函数调用会取回传至panic调用的错误值,恢复正常执行,停止恐慌过程(可以理解为Java中的中断),若recover在defer的函数之外被调用,它将不会停止恐慌过程序列;panic()函数调用后会终止程序,错误情况会被报告,包括引发该恐慌的实参值。
  • 下面给出一个Go语言使用defer+recover()捕获和处理运行时异常的栗子(出现异常后,主函数后面的语句将正常执行):

 

  • 下面给出一个自定义异常的处理,直接使用panic()内建函数终止程序,会输出自定义错误,但后面的代码将不会再执行:

import(
	"fmt"
	"errors"
)

func test1(){
	//使用defer+recover捕获和处理异常
	defer catchError()
	num1 := 10
	num2 := 0
	res := num1/num2
	fmt.Println("res = ",res)
}

//捕获异常
func catchError(){
	err := recover() //recover()也是内建函数
	if err!=nil{	//err!=nil说明捕获到异常
		fmt.Println("异常信息=",err)
	}
}

//支持自定义错误,使用errors.New和panic内建函数
func readConf(name string)(err error){
	if name == "config.ini"{
		return nil //读取返回空...
	} else{
		return errors.New("读取文件错误...")
	}
}

func test(){
	err := readConf("confi.ini")
	if err != nil{
		panic(err) //如果读取文件发生错误,输出错误并终止程序
	}
	fmt.Println("test()继续执行...")
}

func main(){
	test()
	fmt.Println("main()下面的代码......")
}

并发

  • Java中我们只需要了解进程和线程,而Go语言里我们需要知道进程、线程和协程。进程是任务调度的最小单位,每个进程各自都独立一块内存;线程是程序执行流的最小单位,是处理器调度和分派的最小单位;协程是一种用户态的轻量级线程,协程拥有自己的寄存器上下文和栈;
  • 线程和进程都是同步机制,而协程是异步;协程能保留上一次调用时的状态,每次重入相当进入上一次的状态。
  • goroutine是协程的go语言实现,它是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的。
  • Java语言通过Thread类去创建线程,而Go语言通过 go 语句开启一个新的运行期线程, 即 goroutine。
  • Go语言可用通道来传递数据,channel就是用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。默认的通道是不带缓冲区的,如果需要设置缓冲区可以通过make()函数去设置。
  • 需要注意的是出现协程死锁的情况(fatal error: all goroutines are asleep - deadlock),无缓冲信道不会用来存储数据,只会用来给不同协程传输数据。主函数也是一个协程(main goroutine)。下面给出一个简单的demo。
package utils

import(
	"fmt"
	"time"
)

func SaySomething(s string){
	for i:=0;i<5;i++{
		time.Sleep(100 * time.Millisecond)
		fmt.Printf("这是某某第%v次说%v\n",i+1,s)
	}
}

func Sum(s []int, c chan int){
	sum := 0 
	for _,v := range s{
		sum += v
	}
	//把sum发送到通道c
	c <- sum
}

func Fibonacci(n int, c chan int){
	x, y := 0, 1
	for i:=0;i<n;i++{
		//把x发送到通道c中
		c <- x
		x, y = y, x+y
	}
	close(c)
}
package main

import(
	"fmt"
	"goDemo/src/gocode/goroutine/utils"
)

func main(){
	str1 := "hello"
	str2 := "world"
	//输出的 hello 和 world 是没有固定先后顺序。因为它们是两个 goroutine 在执行
	go utils.SaySomething(str1)
	utils.SaySomething(str2)
	fmt.Println("then I can say")

	//通过两个 goroutine 来计算数字之和,在 goroutine 完成计算后,它会计算两个结果的和:
	s := []int{7, 2, 8, -9, 4, 0}

	c := make(chan int)
	fmt.Printf("信道地址=%v,信道容量=%v\n",c, cap(c))
	go utils.Sum(s[:len(s)/2],c)
	go utils.Sum(s[len(s)/2:],c)
	//从通道c接收数据并赋值给x,y
	x, y := <- c , <- c
	fmt.Printf("后半部分求和=%v,前半部分求和=%v,总和=%v\n",x,y,x+y)

	// 这里我们定义了一个可以存储整数类型的带缓冲通道,缓冲区大小为2
    ch := make(chan int, 2)
    // 因为 ch 是带缓冲的通道,我们可以同时发送两个数据
    // 而不用立刻需要去同步读取数据
    ch <- 1
	ch <- 2
	// ch <- 3	//如果再发送一个数据则会出现异常 fatal error: all goroutines are asleep - deadlock!
    // 获取这两个数据
    fmt.Println(<-ch)
	fmt.Println(<-ch)
	// 接收了数据之后缓存里才能继续缓冲数据,否则就会出现协程死锁
	ch <- 3

	//go遍历通道和关闭通道
	cc := make(chan int,10)
	go utils.Fibonacci(cap(cc), cc)

	for i := range cc{
		fmt.Printf("%v\t",i)
	}

	//无缓冲信道不会用来存储数据,只负责数据的流通,两个goroutine可以用无缓冲信道传输数据
	go func(){c <- 1}()
	test := <- c
	fmt.Println(test)
	// c <- 1 //无缓冲信道容量为0,不能在main goroutine中传数据,否则会出现deadlock
}

 

如果你看到这里,那么感谢大佬们花费时间看我这写的不好的笔记,作为一个渣渣觉得学习的过程就是要多思考,我这里面写的并不全面,只是一些基础的内容,也没有很好的重点。从基础到深入还是得自己多钻研,愿大家都能成为业内技术大牛。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值