The difference between .plt and .plt.got is that .plt uses lazy binding and .plt.got uses non-lazy binding.
Lazy binding is possible when all uses of a function are simple function calls. However, if anything requires the address of the function, then non-lazy binding must be used, since binding can only occur when the function is called, and we may need to know the address before the first call. Note that when obtaining the address, the GOT entry is accessed directly; only the function calls go via .plt and .plt.got.
If the -fno-plt compiler option is used, then neither .plt nor .plt.got are emitted, and function calls also directly access the GOT entry.
In the following examples, objdump -d is used for disassembly, and readelf -r is used to list relocations.
.plt
Using x64-64 as an example, .plt will contain entries such as:
0000000000014050 <_Unwind_Resume@plt>:
14050: ff 25 3a e6 0e 00 jmpq *0xee63a(%rip) # 102690 <_Unwind_Resume@GCC_3.0>
14056: 68 02 00 00 00 pushq $0x2
1405b: e9 c0 ff ff ff jmpq 14020 <.plt>
The first jmpq is to the GOT entry, and the second jmpq performs the lazy binding if the GOT entry hasn't been bound yet.
The relocations for .plt's associated GOT entries are in the .rela.plt section and use R_X86_64_JUMP_SLOT, which lets the dynamic linker know these are lazy.
0000000000102690 0000004600000007 R_X86_64_JUMP_SLOT 0000000000000000 _Unwind_Resume@GCC_3.0 + 0
.plt.got
.plt.got contains entries that only need a single jmpq since they aren't lazy:
0000000000014060 <memset@plt>:
14060: ff 25 5a ea 0e 00 jmpq *0xeea5a(%rip) # 102ac0 <memset@GLIBC_2.2.5>
14066: 66 90 xchg %ax,%ax
The relocations for .plt.got's associated GOT entries are in the .rela.dyn section (along with the rest of the GOT relocations), which the dynamic linker binds immediately:
0000000000102ac0 0000004b00000006 R_X86_64_GLOB_DAT 0000000000000000 memset@GLIBC_2.2.5 + 0