内容简介:Spring Aop实现对目标对象的代理,主要有两种方式:Jdk代理和Cglib代理。这两种代理的区别在于,Jdk代理与目标类都会实现同一个接口,并且在代理类中会调用目标类中被代理的方法,调用者实际调用的则是代理类的方法,通过这种方式我们就可以在代理类中织入切面逻辑;Jdk代理存在的问题在于目标类被代理的方法必须实现某个接口,Cglib代理则是为了解决这个问题而存在的,其实现代理的方式是通过为目标类动态生成一个子类,通过在子类中织入相应逻辑来达到织入代理逻辑的目的。关于Jdk代理和Cglib代理,其优缺点
Spring Aop实现对目标对象的代理,主要有两种方式:Jdk代理和Cglib代理。这两种代理的区别在于,Jdk代理与目标类都会实现同一个接口,并且在代理类中会调用目标类中被代理的方法,调用者实际调用的则是代理类的方法,通过这种方式我们就可以在代理类中织入切面逻辑;Jdk代理存在的问题在于目标类被代理的方法必须实现某个接口,Cglib代理则是为了解决这个问题而存在的,其实现代理的方式是通过为目标类动态生成一个子类,通过在子类中织入相应逻辑来达到织入代理逻辑的目的。
关于Jdk代理和Cglib代理,其优缺点主要在于:
- Jdk代理生成的代理类只有一个,因而其编译速度是非常快的;而由于被代理的目标类是动态传入代理类中的,Jdk代理的执行效率相对来说低一点,这也是Jdk代理被称为动态代理的原因;
- Cglib代理需要为每个目标类生成相应的子类,因而在实际运行过程中,其可能会生成非常多的子类,过多的子类始终不是太好的,因为这影响了虚拟机编译类的效率;但由于在调用过程中,代理类的方法是已经静态编译生成了的,因而Cglib代理的执行效率相对来说高一些。
本文主要讲解Spring Aop是如何通过Cglib代理实现将切面逻辑织入目标类的。
1. AopProxy织入对象生成
前面我们讲过,Spring Aop织入切面逻辑的入口方法是 AbstractAutoProxyCreator.createProxy()
方法,如下是该方法的源码:
protected Object createProxy(Class<?> beanClass, @Nullable String beanName, @Nullable Object[] specificInterceptors, TargetSource targetSource) { // 如果当前beanFactory实现了ConfigurableListableBeanFactory接口,则将需要被代理的 // 对象暴露出来 if (this.beanFactory instanceof ConfigurableListableBeanFactory) { AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass); } // 创建代理工厂 ProxyFactory proxyFactory = new ProxyFactory(); // 复制proxyTargetClass,exposeProxy等属性 proxyFactory.copyFrom(this); // 如果当前设置了不使用Cglib代理目标类,则判断目标类是否设置了preserveTargetClass属性, // 如果设置了,则还是强制使用Cglib代理目标类;如果没有设置,则判断目标类是否实现了相关接口, // 没有设置,则还是使用Cglib代理。需要注意的是Spring默认使用的是Jdk代理来织入切面逻辑。 if (!proxyFactory.isProxyTargetClass()) { // 判断目标类是否设置了preserveTargetClass属性 if (shouldProxyTargetClass(beanClass, beanName)) { proxyFactory.setProxyTargetClass(true); } else { // 判断目标类是否实现了相关接口 evaluateProxyInterfaces(beanClass, proxyFactory); } } // 将需要织入的切面逻辑都转换为Advisor对象 Advisor[] advisors = buildAdvisors(beanName, specificInterceptors); proxyFactory.addAdvisors(advisors); proxyFactory.setTargetSource(targetSource); // 提供的hook方法,供子类实现以实现对代理工厂的定制 customizeProxyFactory(proxyFactory); proxyFactory.setFrozen(this.freezeProxy); // 当前判断逻辑默认返回false,子类可进行重写,对于AnnotationAwareAspectJAutoProxyCreator, // 其重写了该方法返回true,因为其已经对获取到的Advisor进行了过滤,后面不需要在对目标类进行重新 // 匹配了 if (advisorsPreFiltered()) { proxyFactory.setPreFiltered(true); } // 生成代理类 return proxyFactory.getProxy(getProxyClassLoader()); }
可以看到,在生成代理类之前,主要做了两件事:①判断使用Jdk代理还是Cglib代理;②设置相关的属性。这里我们继续看最后的 ProxyFactory.getProxy()
方法:
public Object getProxy(@Nullable ClassLoader classLoader) { // 首先获取AopProxy对象,其主要有两个实现:JdkDynamicAopProxy和ObjenesisCglibAopProxy, // 分别用于Jdk和Cglib代理类的生成,其getProxy()方法则用于获取具体的代理对象 return createAopProxy().getProxy(classLoader); }
上面的 createAopProxy()
方法可以理解为一个工厂方法,返回值是一个AopProxy类型的对象,其内部根据具体的条件生成相应的子类对象,即JdkDynamicAopProxy和ObjenesisCglibAopProxy。后面则通过调用 AopProxy.getProxy()
方法获取代理过的对象。如下是 createAopProxy()
方法的实现逻辑:
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException { // 判断当前类是否需要进行运行时优化,或者是指定了使用Cglib代理的方式,再或者是目标类没有用户提供的 // 相关接口,则使用Cglib代理实现代理逻辑的织入 if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) { Class<?> targetClass = config.getTargetClass(); if (targetClass == null) { throw new AopConfigException("TargetSource cannot determine target class: " + "Either an interface or a target is required for proxy creation."); } // 如果被代理的类是一个接口,或者被代理的类是使用Jdk代理生成的类,此时还是使用Jdk代理 if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) { return new JdkDynamicAopProxy(config); } // 返回Cglib代理织入类对象 return new ObjenesisCglibAopProxy(config); } else { // 返回Jdk代理织入类对象 return new JdkDynamicAopProxy(config); } }
这里可以看到,本文需要讲解的Cglib代理逻辑的织入就在 ObjenesisCglibAopProxy.getProxy()
方法中。
2. 代理逻辑的织入
关于代理逻辑的织入,其实现主体还是通过 Enhancer
来实现,即通过需要织入的Advisor列表,生成Callback对象,并将其设置到 Enhancer
对象中,最后通过 Enhancer
生成目标对象。如下是 AopProxy.getProxy()
方法的源码:
public Object getProxy(@Nullable ClassLoader classLoader) { if (logger.isDebugEnabled()) { logger.debug("Creating CGLIB proxy: target source is " + this.advised.getTargetSource()); } try { Class<?> rootClass = this.advised.getTargetClass(); Assert.state(rootClass != null, "Target class must be available for creating a CGLIB proxy"); // 判断当前类是否是已经通过Cglib代理生成的类,如果是的,则获取其原始父类, // 并将其接口设置到需要代理的接口中 Class<?> proxySuperClass = rootClass; if (ClassUtils.isCglibProxyClass(rootClass)) { // 获取父类 proxySuperClass = rootClass.getSuperclass(); // 获取父类实现的接口,并将其设置到需要代理的接口中 Class<?>[] additionalInterfaces = rootClass.getInterfaces(); for (Class<?> additionalInterface : additionalInterfaces) { this.advised.addInterface(additionalInterface); } } // 对目标类进行检查,主要检查点有三个: // 1. 目标方法不能使用final修饰; // 2. 目标方法不能是private类型的; // 3. 目标方法不能是包访问权限的; // 这三个点满足任何一个,当前方法就不能被代理,此时该方法就会被略过 validateClassIfNecessary(proxySuperClass, classLoader); // 创建Enhancer对象,并且设置ClassLoader Enhancer enhancer = createEnhancer(); if (classLoader != null) { enhancer.setClassLoader(classLoader); if (classLoader instanceof SmartClassLoader && ((SmartClassLoader) classLoader).isClassReloadable(proxySuperClass)) { enhancer.setUseCache(false); } } enhancer.setSuperclass(proxySuperClass); // 这里AopProxyUtils.completeProxiedInterfaces()方法的主要目的是为要生成的代理类 // 增加SpringProxy,Advised,DecoratingProxy三个需要实现的接口。这里三个接口的作用如下: // 1. SpringProxy:是一个空接口,用于标记当前生成的代理类是Spring生成的代理类; // 2. Advised:Spring生成代理类所使用的属性都保存在该接口中, // 包括Advisor,Advice和其他相关属性; // 3. DecoratingProxy:该接口用于获取当前代理对象所代理的目标对象的Class类型。 enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised)); enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE); enhancer.setStrategy(new ClassLoaderAwareUndeclaredThrowableStrategy(classLoader)); // 获取当前需要织入到代理类中的逻辑 Callback[] callbacks = getCallbacks(rootClass); Class<?>[] types = new Class<?>[callbacks.length]; for (int x = 0; x < types.length; x++) { types[x] = callbacks[x].getClass(); } // 设置代理类中各个方法将要使用的切面逻辑,这里ProxyCallbackFilter.accept()方法返回 // 的整型值正好一一对应上面Callback数组中各个切面逻辑的下标,也就是说这里的CallbackFilter // 的作用正好指定了代理类中各个方法将要使用Callback数组中的哪个或哪几个切面逻辑 enhancer.setCallbackFilter(new ProxyCallbackFilter( this.advised.getConfigurationOnlyCopy(), this.fixedInterceptorMap, this.fixedInterceptorOffset)); enhancer.setCallbackTypes(types); // 生成代理对象 return createProxyClassAndInstance(enhancer, callbacks); } catch (CodeGenerationException | IllegalArgumentException ex) { throw new AopConfigException("Could not generate CGLIB subclass of class [" + this.advised.getTargetClass() + "]: Common causes of this problem " + "include using a final class or a non-visible class", ex); } catch (Throwable ex) { throw new AopConfigException("Unexpected AOP exception", ex); } }
可以看到,这里的 AopProxy.getProxy()
方法就是生成代理对象的主干逻辑。上面的逻辑中主要有两个部分需要重点讲解:①如果获取Callback数组;②CallbackFilter的作用。关于第一点,我们后面会进行重点讲解,至于第二点,这里我们需要理解的就是CallbackFilter.accept()方法接收一个Method类型的参数,该参数也即当前要生成的代理逻辑的方法,这里的accept()方法将返回目标当前要织入代理逻辑的方法所需要使用的切面逻辑,也即Callback对象在Callback数组中的下标。关于CallbackFilter的使用原理,读者可以阅读 实战CGLib系列之proxy篇(二):回调过滤CallbackFilter
这篇文章。下面我们继续阅读 getCallbacks()
的源码:
private Callback[] getCallbacks(Class<?> rootClass) throws Exception { boolean exposeProxy = this.advised.isExposeProxy(); boolean isFrozen = this.advised.isFrozen(); boolean isStatic = this.advised.getTargetSource().isStatic(); // 用户自定义的代理逻辑的主要织入类 Callback aopInterceptor = new DynamicAdvisedInterceptor(this.advised); Callback targetInterceptor; // 判断如果要暴露代理对象,如果是,则使用AopContext设置将代理对象设置到ThreadLocal中 // 用户则可以通过AopContext获取目标对象 if (exposeProxy) { // 判断被代理的对象是否是静态的,如果是静态的,则将目标对象缓存起来,每次都使用该对象即可, // 如果目标对象是动态的,则在DynamicUnadvisedExposedInterceptor中每次都生成一个新的 // 目标对象,以织入后面的代理逻辑 targetInterceptor = isStatic ? new StaticUnadvisedExposedInterceptor( this.advised.getTargetSource().getTarget()) : new DynamicUnadvisedExposedInterceptor(this.advised.getTargetSource()); } else { // 下面两个类与上面两个的唯一区别就在于是否使用AopContext暴露生成的代理对象 targetInterceptor = isStatic ? new StaticUnadvisedInterceptor(this.advised.getTargetSource().getTarget()) : new DynamicUnadvisedInterceptor(this.advised.getTargetSource()); } // 当前Callback用于一般的不用背代理的方法,这些方法 Callback targetDispatcher = isStatic ? new StaticDispatcher(this.advised.getTargetSource().getTarget()) : new SerializableNoOp(); // 将获取到的callback组装为一个数组 Callback[] mainCallbacks = new Callback[] { aopInterceptor, // 用户自己定义的拦截器 targetInterceptor, // 根据条件是否暴露代理对象的拦截器 new SerializableNoOp(), // 不做任何操作的拦截器 targetDispatcher, this.advisedDispatcher, // 用于存储Advised对象的分发器 new EqualsInterceptor(this.advised), // 针对equals方法调用的拦截器 new HashCodeInterceptor(this.advised) // 针对hashcode方法调用的拦截器 }; Callback[] callbacks; // 如果目标对象是静态的,也即可以缓存的,并且切面逻辑的调用链是固定的, // 则对目标对象和整个调用链进行缓存 if (isStatic && isFrozen) { Method[] methods = rootClass.getMethods(); Callback[] fixedCallbacks = new Callback[methods.length]; this.fixedInterceptorMap = new HashMap<>(methods.length); for (int x = 0; x < methods.length; x++) { // 获取目标对象的切面逻辑 List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice( methods[x], rootClass); fixedCallbacks[x] = new FixedChainStaticTargetInterceptor( chain, this.advised.getTargetSource().getTarget(), this.advised.getTargetClass()); // 对调用链进行缓存 this.fixedInterceptorMap.put(methods[x].toString(), x); } // 将生成的静态调用链存入Callback数组中 callbacks = new Callback[mainCallbacks.length + fixedCallbacks.length]; System.arraycopy(mainCallbacks, 0, callbacks, 0, mainCallbacks.length); System.arraycopy(fixedCallbacks, 0, callbacks, mainCallbacks.length, fixedCallbacks.length); // 这里fixedInterceptorOffset记录了当前静态的调用链的切面逻辑的起始位置, // 这里记录的用处在于后面使用CallbackFilter的时候,如果发现是静态的调用链, // 则直接通过该参数获取相应的调用链,而直接略过了前面的动态调用链 this.fixedInterceptorOffset = mainCallbacks.length; } else { callbacks = mainCallbacks; } return callbacks; }
这里的getCallbacks()方法主要做了三件事:①获取目标对象的动态调用链;②判断是否设置了exposeProxy属性,如果设置了,则生成一个可以暴露代理对象的Callback对象,否则生成一个不做任何处理直接调用目标对象的Callback对象;③判断目标对象是否是静态的,并且当前的切面逻辑是否是固定的,如果是,则将目标对象和调用链进行缓存,以便后续直接调用。这里需要说明的一个点在于第三点,因为在判断目标对象为静态对象,并且调用链是固定的时候,会将目标对象和调用链进行缓存,并且封装到指定的Callback对象中。这里读者可能会疑问为什么动态调用链和静态调用链都进行了缓存,这和前面讲解的CallbackFilter是息息相关的,因为上述代码最后使用fixedInterceptorOffset记录了当前静态调用链在数组中存储的位置,我们前面也讲了,Enhancer可以通过CallbackFilter返回的整数值来动态的指定从当前对象Callback数组中的第几个环绕逻辑开始织入,这里就会使用到fixedInterceptorOffset。从上述代码中可以看出,用户自定义的调用链是在DynamicAdvisedInterceptor中生成的(关于静态调用链的生成实际上是同样的逻辑,只不过静态调用链会被缓存),这里我们看看DynamicAdvisedInterceptor的实现源码:
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { Object oldProxy = null; boolean setProxyContext = false; Object target = null; // 通过TargetSource获取目标对象 TargetSource targetSource = this.advised.getTargetSource(); try { // 判断如果需要暴露代理对象,则将当前代理对象设置到ThreadLocal中 if (this.advised.exposeProxy) { oldProxy = AopContext.setCurrentProxy(proxy); setProxyContext = true; } target = targetSource.getTarget(); Class<?> targetClass = (target != null ? target.getClass() : null); // 获取目标对象切面逻辑的环绕链 List<Object> chain = this.advised .getInterceptorsAndDynamicInterceptionAdvice(method, targetClass); Object retVal; if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) { // 对参数进行处理,以使其与目标方法的参数类型一致,尤其对于数组类型, // 会单独处理其数据类型与实际类型一致 Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args); // 因为没有切面逻辑需要织入,这里直接调用目标方法 retVal = methodProxy.invoke(target, argsToUse); } else { // 通过生成的调用链,对目标方法进行环绕调用 retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed(); } // 对返回值进行处理,如果返回值就是当前目标对象,那么将代理生成的代理对象返回; // 如果返回值为空,并且返回值类型是非void的基本数据类型,则抛出异常; // 如果上述两个条件都不符合,则直接将生成的返回值返回 retVal = processReturnType(proxy, target, method, retVal); return retVal; } finally { // 如果目标对象不是静态的,则调用TargetSource.releaseTarget()方法释放目标对象 if (target != null && !targetSource.isStatic()) { targetSource.releaseTarget(target); } // 将代理对象设置为前面(外层逻辑)调用设置的对象,以防止暴露出来的代理对象不一致 if (setProxyContext) { AopContext.setCurrentProxy(oldProxy); } } }
这里intercept()方法里主要逻辑有两点:①为目标对象生成切面逻辑调用链;②通过切面逻辑对目标对象进行环绕,并且进行调用。关于这两点,我们都会进行讲解,这里我们首先看看Cglib是如何生成调用链的,如下是 getInterceptorsAndDynamicInterceptionAdvice()
方法最终调用的源码,中间略过了部分比较简单的调用:
public List<Object> getInterceptorsAndDynamicInterceptionAdvice( Advised config, Method method, @Nullable Class<?> targetClass) { List<Object> interceptorList = new ArrayList<>(config.getAdvisors().length); Class<?> actualClass = (targetClass != null ? targetClass : method.getDeclaringClass()); // 判断切面逻辑中是否有IntroductionAdvisor类型的Advisor boolean hasIntroductions = hasMatchingIntroductions(config, actualClass); AdvisorAdapterRegistry registry = GlobalAdvisorAdapterRegistry.getInstance(); for (Advisor advisor : config.getAdvisors()) { if (advisor instanceof PointcutAdvisor) { PointcutAdvisor pointcutAdvisor = (PointcutAdvisor) advisor; // 这里判断切面逻辑的调用链是否提前进行过过滤,如果进行过,则不再进行目标方法的匹配, // 如果没有,则再进行一次匹配。这里我们使用的AnnotationAwareAspectJAutoProxyCreator // 在生成切面逻辑的时候就已经进行了过滤,因而这里返回的是true,本文最开始也对这里进行了讲解 if (config.isPreFiltered() || pointcutAdvisor.getPointcut() .getClassFilter().matches(actualClass)) { // 将Advisor对象转换为MethodInterceptor数组 MethodInterceptor[] interceptors = registry.getInterceptors(advisor); MethodMatcher mm = pointcutAdvisor.getPointcut().getMethodMatcher(); // 这里进行匹配的时候,首先会检查是否为IntroductionAwareMethodMatcher类型的 // Matcher,如果是,则调用其定义的matches()方法进行匹配,如果不是,则直接调用 // 当前切面的matches()方法进行匹配。这里由于前面进行匹配时可能存在部分在静态匹配时 // 无法确认的方法匹配结果,因而这里调用是必要的,而对于能够确认的匹配逻辑,这里调用 // 也是非常迅速的,因为前面已经对匹配结果进行了缓存 if (MethodMatchers.matches(mm, method, actualClass, hasIntroductions)) { // 判断如果是动态匹配,则使用InterceptorAndDynamicMethodMatcher对其进行封装 if (mm.isRuntime()) { for (MethodInterceptor interceptor : interceptors) { interceptorList.add( new InterceptorAndDynamicMethodMatcher(interceptor, mm)); } } else { // 如果是静态匹配,则直接将调用链返回 interceptorList.addAll(Arrays.asList(interceptors)); } } } } else if (advisor instanceof IntroductionAdvisor) { IntroductionAdvisor ia = (IntroductionAdvisor) advisor; // 判断如果为IntroductionAdvisor类型的Advisor,则将调用链封装为Interceptor数组 if (config.isPreFiltered() || ia.getClassFilter().matches(actualClass)) { Interceptor[] interceptors = registry.getInterceptors(advisor); interceptorList.addAll(Arrays.asList(interceptors)); } } else { // 这里是提供的使用自定义的转换器对Advisor进行转换的逻辑,因为getInterceptors()方法中 // 会使用相应的Adapter对目标Advisor进行匹配,如果能匹配上,通过其getInterceptor()方法 // 将自定义的Advice转换为MethodInterceptor对象 Interceptor[] interceptors = registry.getInterceptors(advisor); interceptorList.addAll(Arrays.asList(interceptors)); } } return interceptorList; }
这里获取调用链的逻辑其实比较简单,其最终的目的就是将Advisor数组一个一个的封装为Interceptor对象。在进行Advisor封装的时候,这里分为了三种类型:
- 如果目标切面逻辑是一般的切面逻辑,即PointcutAdvisor,则会在运行时对目标方法进行动态匹配,因为前面可能存在还不能确认的是否应该应用切面逻辑的方法;
- 如果切面逻辑是IntroductionAdvisor的,则将其封装为Interceptor类型的数组;
- 如果以上两个都不是,说明切面逻辑可能是用户自定义的切面逻辑,这里就通过注册的AdvisorAdapter进行匹配,如果某个Adapter能够支持当前Advisor的转换,则调用其getInterceptor()方法将Advisor转换为MethodInterceptor返回。
下面我们看看Cglib是如何通过生成的切面调用链将目标对象进行环绕的。前面我们讲了,将切面逻辑进行织入的逻辑在 CglibMethodInvocation
中,实际上其调用逻辑在其 proceed()
方法中,这里我们直接看该方法的源码:
public Object proceed() throws Throwable { // 这里currentInterceptorIndex记录了当前调用链中正在调用的Intercepor的下标,该数值初始为-1 if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) { // 如果调用链为空,则直接调用目标方法 return invokeJoinpoint(); } // 获取下一个需要织入的Interceptor逻辑 Object interceptorOrInterceptionAdvice = this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex); if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) { InterceptorAndDynamicMethodMatcher dm = (InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice; // 对动态的方法进行匹配,如果匹配成功,才进行调用,否则直接进行下一个Interceptor的调用 if (dm.methodMatcher.matches(this.method, this.targetClass, this.arguments)) { return dm.interceptor.invoke(this); } else { return proceed(); } } else { // 如果不需要进行动态匹配,则直接进行下一步的调用 return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this); } }
这里 proceed()
方法的逻辑比较简单,其使用一个索引记录了当前正在调用的Interceptor在调用链中的位置,并且依次对调用链进行调用,从而实现将切面逻辑织入目标对象的目的。这里最终对目标对象的调用的逻辑在 invokeJoinpoint()
方法中。
3. 小结
本文首先讲解Spring是如何通过配置的参数来选择使用哪种代理方式的,然后重点讲解了Spring Aop是如何使用Cglib代理实现代理逻辑的织入的。
以上所述就是小编给大家介绍的《原 荐 Spring Aop之Cglib实现原理详解》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Go程序设计语言
艾伦 A. A. 多诺万 / 李道兵、高博、庞向才、金鑫鑫、林齐斌 / 机械工业出版社 / 2017-5 / 79
本书由《C程序设计语言》的作者Kernighan和谷歌公司Go团队主管Alan Donovan联袂撰写,是学习Go语言程序设计的指南。本书共13章,主要内容包括:Go的基础知识、基本结构、基本数据类型、复合数据类型、函数、方法、接口、goroutine、通道、共享变量的并发性、包、go工具、测试、反射等。 本书适合作为计算机相关专业的教材,也可供Go语言爱好者阅读。一起来看看 《Go程序设计语言》 这本书的介绍吧!